diff --git a/api/add_office.php b/api/add_office.php new file mode 100644 index 0000000..7100a69 --- /dev/null +++ b/api/add_office.php @@ -0,0 +1,33 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +// Basic validation +if (empty($_POST['nama_kantor']) || empty($_POST['tipe_kantor'])) { + echo json_encode(['success' => false, 'message' => 'Nama kantor and tipe are required.']); + exit; +} + +$nama_kantor = $_POST['nama_kantor']; +$alamat = $_POST['alamat'] ?? null; +$tipe_kantor = $_POST['tipe_kantor']; + +try { + $sql = "INSERT INTO kantor (nama_kantor, alamat, tipe_kantor) VALUES (?, ?, ?)"; + $stmt = db()->prepare($sql); + $stmt->execute([$nama_kantor, $alamat, $tipe_kantor]); + + echo json_encode(['success' => true, 'message' => 'Office added successfully.']); + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/add_user.php b/api/add_user.php new file mode 100644 index 0000000..72629f6 --- /dev/null +++ b/api/add_user.php @@ -0,0 +1,48 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +// Basic validation +if (empty($_POST['nama_lengkap']) || empty($_POST['email']) || empty($_POST['password']) || empty($_POST['role'])) { + echo json_encode(['success' => false, 'message' => 'Please fill all required fields.']); + exit; +} + +$nama_lengkap = $_POST['nama_lengkap']; +$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL); +$password = password_hash($_POST['password'], PASSWORD_DEFAULT); +$role = $_POST['role']; +$id_kantor = !empty($_POST['id_kantor']) ? filter_input(INPUT_POST, 'id_kantor', FILTER_SANITIZE_NUMBER_INT) : null; + +if (!$email) { + echo json_encode(['success' => false, 'message' => 'Invalid email format.']); + exit; +} + +// Check for existing email +try { + $stmt = db()->prepare("SELECT id FROM users WHERE email = ?"); + $stmt->execute([$email]); + if ($stmt->fetch()) { + echo json_encode(['success' => false, 'message' => 'Email already exists.']); + exit; + } + + $sql = "INSERT INTO users (nama_lengkap, email, password, role, id_kantor) VALUES (?, ?, ?, ?, ?)"; + $stmt = db()->prepare($sql); + $stmt->execute([$nama_lengkap, $email, $password, $role, $id_kantor]); + + echo json_encode(['success' => true, 'message' => 'User added successfully.']); + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/delete_asset.php b/api/delete_asset.php new file mode 100644 index 0000000..d239d60 --- /dev/null +++ b/api/delete_asset.php @@ -0,0 +1,42 @@ + false, 'message' => 'Akses ditolak.']); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +$data = json_decode(file_get_contents('php://input'), true); +$assetId = $data['id'] ?? null; + +if (!$assetId) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'ID Aset tidak valid.']); + exit; +} + +try { + $pdo = db(); + $stmt = $pdo->prepare("DELETE FROM aset WHERE id = ?"); + $stmt->execute([$assetId]); + + if ($stmt->rowCount() > 0) { + echo json_encode(['success' => true, 'message' => 'Aset berhasil dihapus.']); + } else { + http_response_code(404); + echo json_encode(['success' => false, 'message' => 'Aset tidak ditemukan.']); + } +} catch (PDOException $e) { + http_response_code(500); + // In production, log this error instead of echoing it. + echo json_encode(['success' => false, 'message' => 'Gagal menghapus aset: ' . $e->getMessage()]); +} diff --git a/api/delete_office.php b/api/delete_office.php new file mode 100644 index 0000000..67217b8 --- /dev/null +++ b/api/delete_office.php @@ -0,0 +1,46 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +if (empty($_POST['id'])) { + echo json_encode(['success' => false, 'message' => 'Office ID is required.']); + exit; +} + +$id = filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT); + +try { + // The foreign key constraints in `users` and `aset` tables are set to + // `ON DELETE SET NULL` and `ON DELETE RESTRICT` respectively. + // We should check for assets before deleting. + + $stmt_check = db()->prepare("SELECT COUNT(*) FROM aset WHERE id_kantor_lokasi = ?"); + $stmt_check->execute([$id]); + if ($stmt_check->fetchColumn() > 0) { + echo json_encode(['success' => false, 'message' => 'Cannot delete office. It is still associated with existing assets. Please reassign assets first.']); + exit; + } + + // If no assets are linked, proceed with deletion. + // Users linked to this office will have their id_kantor set to NULL. + $stmt_delete = db()->prepare("DELETE FROM kantor WHERE id = ?"); + $stmt_delete->execute([$id]); + + if ($stmt_delete->rowCount() > 0) { + echo json_encode(['success' => true, 'message' => 'Office deleted successfully.']); + } else { + echo json_encode(['success' => false, 'message' => 'Office not found or could not be deleted.']); + } + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/delete_user.php b/api/delete_user.php new file mode 100644 index 0000000..f8c4984 --- /dev/null +++ b/api/delete_user.php @@ -0,0 +1,42 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +if (empty($_POST['id'])) { + echo json_encode(['success' => false, 'message' => 'User ID is required.']); + exit; +} + +$id = filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT); + +// Prevent super admin from deleting themselves +if ($id == $_SESSION['user_id']) { + echo json_encode(['success' => false, 'message' => 'You cannot delete your own account.']); + exit; +} + +try { + // Optional: Check if user has related records (e.g., assets) before deleting + // For now, we rely on the database foreign key constraint (ON DELETE SET NULL) + + $stmt = db()->prepare("DELETE FROM users WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() > 0) { + echo json_encode(['success' => true, 'message' => 'User deleted successfully.']); + } else { + echo json_encode(['success' => false, 'message' => 'User not found or could not be deleted.']); + } + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/get_asset.php b/api/get_asset.php new file mode 100644 index 0000000..73cf3d7 --- /dev/null +++ b/api/get_asset.php @@ -0,0 +1,35 @@ + 'Akses ditolak.']); + exit; +} + +$assetId = $_GET['id'] ?? null; + +if (!$assetId) { + http_response_code(400); + echo json_encode(['error' => 'ID Aset tidak valid.']); + exit; +} + +try { + $pdo = db(); + $stmt = $pdo->prepare("SELECT * FROM aset WHERE id = ?"); + $stmt->execute([$assetId]); + $asset = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($asset) { + header('Content-Type: application/json'); + echo json_encode($asset); + } else { + http_response_code(404); + echo json_encode(['error' => 'Aset tidak ditemukan.']); + } +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Gagal mengambil data aset: ' . $e->getMessage()]); +} diff --git a/api/get_offices.php b/api/get_offices.php new file mode 100644 index 0000000..87bc3ad --- /dev/null +++ b/api/get_offices.php @@ -0,0 +1,32 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +try { + $sql = "SELECT * FROM kantor ORDER BY nama_kantor ASC"; + $params = []; + + // If an ID is provided, fetch a single office + if (isset($_GET['id']) && !empty($_GET['id'])) { + $sql = "SELECT * FROM kantor WHERE id = ?"; + $params[] = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT); + } + + $stmt = db()->prepare($sql); + $stmt->execute($params); + $offices = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode(['success' => true, 'offices' => $offices]); + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/get_user.php b/api/get_user.php new file mode 100644 index 0000000..93ed25c --- /dev/null +++ b/api/get_user.php @@ -0,0 +1,34 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +if (!isset($_GET['id'])) { + echo json_encode(['success' => false, 'message' => 'User ID is required']); + exit; +} + +$id = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT); + +try { + $stmt = db()->prepare("SELECT id, nama_lengkap, email, role, id_kantor FROM users WHERE id = ?"); + $stmt->execute([$id]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($user) { + echo json_encode(['success' => true, 'user' => $user]); + } else { + echo json_encode(['success' => false, 'message' => 'User not found']); + } + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/get_users.php b/api/get_users.php new file mode 100644 index 0000000..f2a46af --- /dev/null +++ b/api/get_users.php @@ -0,0 +1,26 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +try { + $sql = "SELECT u.id, u.nama_lengkap, u.email, u.role, u.created_at, k.nama_kantor + FROM users u + LEFT JOIN kantor k ON u.id_kantor = k.id + ORDER BY u.nama_lengkap ASC"; + $stmt = db()->query($sql); + $users = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode(['success' => true, 'users' => $users]); + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/update_asset.php b/api/update_asset.php new file mode 100644 index 0000000..ee0e9e6 --- /dev/null +++ b/api/update_asset.php @@ -0,0 +1,73 @@ + false, 'message' => 'Metode tidak diizinkan.']); + exit; +} + +if (!isset($_SESSION['user_id'])) { + http_response_code(403); + echo json_encode(['success' => false, 'message' => 'Akses ditolak.']); + exit; +} + +// Get data from POST request +$id = $_POST['id'] ?? null; +$nama_aset = $_POST['nama_aset'] ?? ''; +$id_kategori = $_POST['id_kategori'] ?? null; +$id_kantor_lokasi = $_POST['id_kantor_lokasi'] ?? null; +$spesifikasi = $_POST['spesifikasi'] ?? ''; +$tanggal_pembelian = $_POST['tanggal_pembelian'] ?? null; +$harga_pembelian = $_POST['harga_pembelian'] ?? null; +$vendor = $_POST['vendor'] ?? ''; +$status = $_POST['status'] ?? ''; + +// Basic validation +if (empty($id) || empty($nama_aset) || empty($id_kategori) || empty($id_kantor_lokasi) || empty($tanggal_pembelian) || empty($harga_pembelian) || empty($status)) { + http_response_code(400); + echo json_encode(['success' => false, 'message' => 'Semua field yang wajib diisi harus diisi.']); + exit; +} + +try { + $pdo = db(); + $sql = "UPDATE aset SET + nama_aset = :nama_aset, + id_kategori = :id_kategori, + id_kantor_lokasi = :id_kantor_lokasi, + spesifikasi = :spesifikasi, + tanggal_pembelian = :tanggal_pembelian, + harga_pembelian = :harga_pembelian, + vendor = :vendor, + status = :status, + updated_at = NOW() + WHERE id = :id"; + + $stmt = $pdo->prepare($sql); + + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->bindParam(':nama_aset', $nama_aset, PDO::PARAM_STR); + $stmt->bindParam(':id_kategori', $id_kategori, PDO::PARAM_INT); + $stmt->bindParam(':id_kantor_lokasi', $id_kantor_lokasi, PDO::PARAM_INT); + $stmt->bindParam(':spesifikasi', $spesifikasi, PDO::PARAM_STR); + $stmt->bindParam(':tanggal_pembelian', $tanggal_pembelian, PDO::PARAM_STR); + $stmt->bindParam(':harga_pembelian', $harga_pembelian); + $stmt->bindParam(':vendor', $vendor, PDO::PARAM_STR); + $stmt->bindParam(':status', $status, PDO::PARAM_STR); + + if ($stmt->execute()) { + echo json_encode(['success' => true, 'message' => 'Aset berhasil diperbarui.']); + } else { + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Gagal memperbarui aset.']); + } +} catch (PDOException $e) { + http_response_code(500); + // In production, log this error. + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} diff --git a/api/update_office.php b/api/update_office.php new file mode 100644 index 0000000..5656ba0 --- /dev/null +++ b/api/update_office.php @@ -0,0 +1,34 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +// Basic validation +if (empty($_POST['id']) || empty($_POST['nama_kantor']) || empty($_POST['tipe_kantor'])) { + echo json_encode(['success' => false, 'message' => 'Incomplete data for update.']); + exit; +} + +$id = filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT); +$nama_kantor = $_POST['nama_kantor']; +$alamat = $_POST['alamat'] ?? null; +$tipe_kantor = $_POST['tipe_kantor']; + +try { + $sql = "UPDATE kantor SET nama_kantor = ?, alamat = ?, tipe_kantor = ? WHERE id = ?"; + $stmt = db()->prepare($sql); + $stmt->execute([$nama_kantor, $alamat, $tipe_kantor, $id]); + + echo json_encode(['success' => true, 'message' => 'Office updated successfully.']); + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/api/update_user.php b/api/update_user.php new file mode 100644 index 0000000..e7ba7de --- /dev/null +++ b/api/update_user.php @@ -0,0 +1,56 @@ + false, 'message' => 'Unauthorized']); + exit; +} + +// Basic validation +if (empty($_POST['id']) || empty($_POST['nama_lengkap']) || empty($_POST['email']) || empty($_POST['role'])) { + echo json_encode(['success' => false, 'message' => 'Incomplete data for update.']); + exit; +} + +$id = filter_input(INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT); +$nama_lengkap = $_POST['nama_lengkap']; +$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL); +$role = $_POST['role']; +$id_kantor = !empty($_POST['id_kantor']) ? filter_input(INPUT_POST, 'id_kantor', FILTER_SANITIZE_NUMBER_INT) : null; + +if (!$email) { + echo json_encode(['success' => false, 'message' => 'Invalid email format.']); + exit; +} + +try { + // Check if email is used by another user + $stmt = db()->prepare("SELECT id FROM users WHERE email = ? AND id != ?"); + $stmt->execute([$email, $id]); + if ($stmt->fetch()) { + echo json_encode(['success' => false, 'message' => 'Email is already in use by another account.']); + exit; + } + + // Handle password update + if (!empty($_POST['password'])) { + $password = password_hash($_POST['password'], PASSWORD_DEFAULT); + $sql = "UPDATE users SET nama_lengkap = ?, email = ?, password = ?, role = ?, id_kantor = ? WHERE id = ?"; + $stmt = db()->prepare($sql); + $stmt->execute([$nama_lengkap, $email, $password, $role, $id_kantor, $id]); + } else { + $sql = "UPDATE users SET nama_lengkap = ?, email = ?, role = ?, id_kantor = ? WHERE id = ?"; + $stmt = db()->prepare($sql); + $stmt->execute([$nama_lengkap, $email, $role, $id_kantor, $id]); + } + + echo json_encode(['success' => true, 'message' => 'User updated successfully.']); + +} catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 4567512..ed9414e 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,13 +1,15 @@ - document.addEventListener('DOMContentLoaded', function () { // --- Constants --- const API_BASE = 'api/'; const addAssetForm = document.getElementById('addAssetForm'); + const editAssetForm = document.getElementById('editAssetForm'); const assetsTableBody = document.getElementById('assets-table-body'); const loadingIndicator = document.getElementById('loading-indicator'); const notificationToastEl = document.getElementById('notificationToast'); const notificationToast = new bootstrap.Toast(notificationToastEl); + const editAssetModalEl = document.getElementById('editAssetModal'); + const editAssetModal = new bootstrap.Modal(editAssetModalEl); // --- Functions --- @@ -20,8 +22,7 @@ document.addEventListener('DOMContentLoaded', function () { function showToast(title, body, type = 'success') { const toastHeader = notificationToastEl.querySelector('.toast-header'); - // Reset classes - toastHeader.classList.remove('bg-success', 'text-white', 'bg-danger'); + toastHeader.classList.remove('bg-success', 'text-white', 'bg-danger', 'text-white'); if (type === 'success') { toastHeader.classList.add('bg-success', 'text-white'); @@ -37,40 +38,45 @@ document.addEventListener('DOMContentLoaded', function () { /** * Fetches data from the API. * @param {string} endpoint - The API endpoint to fetch from. + * @param {object} options - Optional fetch options. * @returns {Promise} - The JSON response. */ - async function fetchApi(endpoint) { + async function fetchApi(endpoint, options = {}) { try { - const response = await fetch(API_BASE + endpoint); + const response = await fetch(API_BASE + endpoint, options); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const errorData = await response.json().catch(() => ({ message: 'Gagal memuat detail error.' })); + throw new Error(errorData.message || `HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error(`Error fetching ${endpoint}:`, error); - showToast('Error', `Gagal memuat data dari server: ${error.message}`, 'error'); + showToast('Error', `Terjadi kesalahan: ${error.message}`, 'error'); return null; } } /** - * Populates select dropdowns. + * Populates select dropdowns for both add and edit modals. */ async function populateSelectOptions() { const data = await fetchApi('get_options.php'); if (!data) return; - const kategoriSelect = document.getElementById('id_kategori'); - const kantorSelect = document.getElementById('id_kantor_lokasi'); + const selects = [ + { el: document.getElementById('id_kategori'), data: data.kategori, placeholder: 'Pilih Kategori...', name: 'nama_kategori' }, + { el: document.getElementById('id_kantor_lokasi'), data: data.kantor, placeholder: 'Pilih Lokasi...', name: 'nama_kantor' }, + { el: document.getElementById('edit_id_kategori'), data: data.kategori, placeholder: 'Pilih Kategori...', name: 'nama_kategori' }, + { el: document.getElementById('edit_id_kantor_lokasi'), data: data.kantor, placeholder: 'Pilih Lokasi...', name: 'nama_kantor' } + ]; - kategoriSelect.innerHTML = ''; - data.kategori.forEach(item => { - kategoriSelect.innerHTML += ``; - }); - - kantorSelect.innerHTML = ''; - data.kantor.forEach(item => { - kantorSelect.innerHTML += ``; + selects.forEach(s => { + if (s.el) { + s.el.innerHTML = ``; + s.data.forEach(item => { + s.el.innerHTML += ``; + }); + } }); } @@ -93,10 +99,14 @@ document.addEventListener('DOMContentLoaded', function () { ${asset.nama_aset} ${asset.nama_kategori} ${asset.nama_kantor} - ${asset.status} + ${asset.status} - - + + `; @@ -107,12 +117,11 @@ document.addEventListener('DOMContentLoaded', function () { } else { assetsTableBody.innerHTML = 'Gagal memuat data aset.'; } - feather.replace(); // Re-initialize Feather icons + feather.replace(); } /** * Handles the submission of the 'Add Asset' form. - * @param {Event} event - The form submission event. */ async function handleAddAssetSubmit(event) { event.preventDefault(); @@ -128,41 +137,122 @@ document.addEventListener('DOMContentLoaded', function () { submitButton.disabled = true; submitButton.innerHTML = ''' Menyimpan...'''; - try { - const response = await fetch(API_BASE + 'add_asset.php', { - method: 'POST', - body: formData - }); + const result = await fetchApi('add_asset.php', { method: 'POST', body: formData }); - const result = await response.json(); + if (result && result.success) { + showToast('Sukses', 'Aset baru berhasil ditambahkan.'); + addAssetForm.reset(); + addAssetForm.classList.remove('was-validated'); + bootstrap.Modal.getInstance(document.getElementById('addAssetModal')).hide(); + loadAssets(); + } else if (result) { + showToast('Error', result.message, 'error'); + } + + submitButton.disabled = false; + submitButton.innerHTML = 'Simpan Aset'; + } - if (result.success) { - showToast('Sukses', 'Aset baru berhasil ditambahkan.'); - addAssetForm.reset(); - addAssetForm.classList.remove('was-validated'); - bootstrap.Modal.getInstance(document.getElementById('addAssetModal')).hide(); - loadAssets(); // Refresh the table - } else { - showToast('Error', result.message || 'Terjadi kesalahan yang tidak diketahui.', 'error'); - } - } catch (error) { - console.error('Error submitting form:', error); - showToast('Error', `Gagal mengirim data: ${error.message}`, 'error'); - } finally { - submitButton.disabled = false; - submitButton.innerHTML = 'Simpan Aset'; + /** + * Opens the edit modal and populates it with asset data. + * @param {number} assetId - The ID of the asset to edit. + */ + async function openEditModal(assetId) { + const asset = await fetchApi(`get_asset.php?id=${assetId}`); + if (!asset) return; + + document.getElementById('edit_id').value = asset.id; + document.getElementById('edit_nama_aset').value = asset.nama_aset; + document.getElementById('edit_id_kategori').value = asset.id_kategori; + document.getElementById('edit_id_kantor_lokasi').value = asset.id_kantor_lokasi; + document.getElementById('edit_spesifikasi').value = asset.spesifikasi; + document.getElementById('edit_tanggal_pembelian').value = asset.tanggal_pembelian; + document.getElementById('edit_harga_pembelian').value = asset.harga_pembelian; + document.getElementById('edit_vendor').value = asset.vendor; + document.getElementById('edit_status').value = asset.status; + + editAssetModal.show(); + } + + /** + * Handles the submission of the 'Edit Asset' form. + */ + async function handleEditAssetSubmit(event) { + event.preventDefault(); + event.stopPropagation(); + + if (!editAssetForm.checkValidity()) { + editAssetForm.classList.add('was-validated'); + return; + } + + const formData = new FormData(editAssetForm); + const submitButton = editAssetForm.querySelector('button[type="submit"]'); + submitButton.disabled = true; + submitButton.innerHTML = ''' Menyimpan...'''; + + const result = await fetchApi('update_asset.php', { method: 'POST', body: formData }); + + if (result && result.success) { + showToast('Sukses', 'Aset berhasil diperbarui.'); + editAssetModal.hide(); + loadAssets(); + } else if (result) { + showToast('Error', result.message, 'error'); + } + + submitButton.disabled = false; + submitButton.innerHTML = 'Simpan Perubahan'; + } + + /** + * Handles the deletion of an asset. + * @param {number} assetId - The ID of the asset to delete. + */ + async function handleDeleteAsset(assetId) { + if (!confirm('Anda yakin ingin menghapus aset ini? Tindakan ini tidak dapat dibatalkan.')) { + return; + } + + const result = await fetchApi('delete_asset.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: assetId }) + }); + + if (result && result.success) { + showToast('Sukses', 'Aset berhasil dihapus.'); + loadAssets(); + } else if (result) { + showToast('Error', result.message, 'error'); } } // --- Initializations --- - feather.replace(); // Initial call for icons + feather.replace(); populateSelectOptions(); loadAssets(); // --- Event Listeners --- addAssetForm.addEventListener('submit', handleAddAssetSubmit); + editAssetForm.addEventListener('submit', handleEditAssetSubmit); + + assetsTableBody.addEventListener('click', function(event) { + const editButton = event.target.closest('.btn-edit'); + const deleteButton = event.target.closest('.btn-delete'); + + if (editButton) { + const assetId = editButton.dataset.assetId; + openEditModal(assetId); + } + + if (deleteButton) { + const assetId = deleteButton.dataset.assetId; + handleDeleteAsset(assetId); + } + }); // Add feather icon class for easier styling document.querySelectorAll('i[data-feather]').forEach(el => { @@ -171,4 +261,4 @@ document.addEventListener('DOMContentLoaded', function () { el.classList.add(`icon-${iconName}`); }); -}); +}); \ No newline at end of file diff --git a/assets/js/offices.js b/assets/js/offices.js new file mode 100644 index 0000000..964166c --- /dev/null +++ b/assets/js/offices.js @@ -0,0 +1,114 @@ +document.addEventListener('DOMContentLoaded', function () { + const officeModal = new bootstrap.Modal(document.getElementById('office-modal')); + const officeForm = document.getElementById('office-form'); + const officeTable = document.getElementById('offices-table').getElementsByTagName('tbody')[0]; + const modalLabel = document.getElementById('office-modal-label'); + + // Function to fetch and display offices + function loadOffices() { + fetch('api/get_offices.php') + .then(response => response.json()) + .then(data => { + officeTable.innerHTML = ''; + if(data.success) { + data.offices.forEach(office => { + const row = officeTable.insertRow(); + row.innerHTML = ` + ${office.nama_kantor} + ${office.alamat} + ${office.tipe_kantor} + + + + + `; + }); + feather.replace(); + } + }) + .catch(error => console.error('Error loading offices:', error)); + } + + // Show modal for adding a new office + document.getElementById('btn-add-office').addEventListener('click', function () { + officeForm.reset(); + document.getElementById('office-id').value = ''; + modalLabel.textContent = 'Tambah Kantor Baru'; + officeModal.show(); + }); + + // Handle form submission for both add and edit + officeForm.addEventListener('submit', function (e) { + e.preventDefault(); + const formData = new FormData(officeForm); + const officeId = formData.get('id'); + const url = officeId ? 'api/update_office.php' : 'api/add_office.php'; + + fetch(url, { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + officeModal.hide(); + loadOffices(); // Refresh the table + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => console.error('Form submission error:', error)); + }); + + // Handle edit and delete button clicks + officeTable.addEventListener('click', function (e) { + const target = e.target.closest('button'); + if (!target) return; + + const id = target.dataset.id; + + // Edit button + if (target.classList.contains('edit-btn')) { + // Fetch full details to edit + fetch(`api/get_offices.php?id=${id}`) // We can reuse get_offices and filter by id server-side + .then(response => response.json()) + .then(data => { + if (data.success && data.offices.length > 0) { + const office = data.offices[0]; + document.getElementById('office-id').value = office.id; + document.getElementById('nama_kantor').value = office.nama_kantor; + document.getElementById('alamat').value = office.alamat; + document.getElementById('tipe_kantor').value = office.tipe_kantor; + + modalLabel.textContent = 'Edit Kantor'; + officeModal.show(); + } else { + alert('Error: Could not fetch office details.'); + } + }); + } + + // Delete button + if (target.classList.contains('delete-btn')) { + if (confirm('Apakah Anda yakin ingin menghapus kantor ini? Ini mungkin akan mempengaruhi pengguna dan aset yang terkait.')) { + fetch('api/delete_office.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `id=${id}` + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + loadOffices(); // Refresh the table + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => console.error('Delete error:', error)); + } + } + }); + + // Initial load of offices + loadOffices(); +}); diff --git a/assets/js/users.js b/assets/js/users.js new file mode 100644 index 0000000..b212150 --- /dev/null +++ b/assets/js/users.js @@ -0,0 +1,128 @@ +document.addEventListener('DOMContentLoaded', function () { + const userModal = new bootstrap.Modal(document.getElementById('user-modal')); + const userForm = document.getElementById('user-form'); + const userTable = document.getElementById('users-table').getElementsByTagName('tbody')[0]; + const modalLabel = document.getElementById('user-modal-label'); + const passwordField = document.getElementById('password'); + const passwordHelpText = passwordField.nextElementSibling; + + // Function to fetch and display users + function loadUsers() { + fetch('api/get_users.php') + .then(response => response.json()) + .then(data => { + userTable.innerHTML = ''; + if(data.success) { + data.users.forEach(user => { + const row = userTable.insertRow(); + row.innerHTML = ` + ${user.nama_lengkap} + ${user.email} + ${user.role.replace('_', ' ')} + ${user.nama_kantor || 'N/A'} + ${new Date(user.created_at).toLocaleDateString()} + + + + + `; + }); + feather.replace(); + } + }) + .catch(error => console.error('Error loading users:', error)); + } + + // Show modal for adding a new user + document.getElementById('btn-add-user').addEventListener('click', function () { + userForm.reset(); + document.getElementById('user-id').value = ''; + modalLabel.textContent = 'Tambah Pengguna Baru'; + passwordField.setAttribute('required', 'required'); + passwordHelpText.style.display = 'none'; + userModal.show(); + }); + + // Handle form submission for both add and edit + userForm.addEventListener('submit', function (e) { + e.preventDefault(); + const formData = new FormData(userForm); + const userId = formData.get('id'); + const url = userId ? 'api/update_user.php' : 'api/add_user.php'; + + // If password is empty on update, remove it from form data + if (userId && !formData.get('password')) { + formData.delete('password'); + } + + fetch(url, { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + userModal.hide(); + loadUsers(); // Refresh the table + // You can add a toast notification here for better UX + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => console.error('Form submission error:', error)); + }); + + // Handle edit and delete button clicks + userTable.addEventListener('click', function (e) { + const target = e.target.closest('button'); + if (!target) return; + + const id = target.dataset.id; + + // Edit button + if (target.classList.contains('edit-btn')) { + fetch(`api/get_user.php?id=${id}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + const user = data.user; + document.getElementById('user-id').value = user.id; + document.getElementById('nama_lengkap').value = user.nama_lengkap; + document.getElementById('email').value = user.email; + document.getElementById('role').value = user.role; + document.getElementById('id_kantor').value = user.id_kantor || ''; + + modalLabel.textContent = 'Edit Pengguna'; + passwordField.removeAttribute('required'); + passwordHelpText.style.display = 'block'; + userModal.show(); + } else { + alert('Error: ' + data.message); + } + }); + } + + // Delete button + if (target.classList.contains('delete-btn')) { + if (confirm('Apakah Anda yakin ingin menghapus pengguna ini?')) { + fetch('api/delete_user.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `id=${id}` + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + loadUsers(); // Refresh the table + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => console.error('Delete error:', error)); + } + } + }); + + // Initial load of users + loadUsers(); +}); diff --git a/auth.php b/auth.php new file mode 100644 index 0000000..863babf --- /dev/null +++ b/auth.php @@ -0,0 +1,46 @@ +prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + // Regenerate session ID to prevent session fixation + session_regenerate_id(true); + + $_SESSION['user_id'] = $user['id']; + $_SESSION['user_name'] = $user['nama_lengkap']; + $_SESSION['user_role'] = $user['role']; + $_SESSION['user_office_id'] = $user['id_kantor']; + + header('Location: index.php'); + exit(); + } else { + $_SESSION['login_error'] = 'Email atau password salah.'; + header('Location: login.php'); + exit(); + } + } catch (PDOException $e) { + // In a real app, you would log this error + $_SESSION['login_error'] = 'Terjadi kesalahan pada database.'; + header('Location: login.php'); + exit(); + } +} else { + header('Location: login.php'); + exit(); +} diff --git a/db/setup.php b/db/setup.php index e03f484..40fa048 100644 --- a/db/setup.php +++ b/db/setup.php @@ -50,6 +50,21 @@ try { UNIQUE KEY `kode_kategori` (`kode_kategori`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `nama_lengkap` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, + `role` enum('super_admin','admin_cabang','pegawai') NOT NULL DEFAULT 'pegawai', + `id_kantor` int(11) DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + KEY `id_kantor` (`id_kantor`), + CONSTRAINT `users_ibfk_1` FOREIGN KEY (`id_kantor`) REFERENCES `kantor` (`id`) ON DELETE SET NULL ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS `aset` ( `id` int(11) NOT NULL AUTO_INCREMENT, `kode_aset` varchar(255) NOT NULL, @@ -68,13 +83,15 @@ try { UNIQUE KEY `kode_aset` (`kode_aset`), KEY `id_kategori` (`id_kategori`), KEY `id_kantor_lokasi` (`id_kantor_lokasi`), + KEY `id_pengguna_penanggung_jawab` (`id_pengguna_penanggung_jawab`), CONSTRAINT `aset_ibfk_1` FOREIGN KEY (`id_kategori`) REFERENCES `kategori_aset` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, - CONSTRAINT `aset_ibfk_2` FOREIGN KEY (`id_kantor_lokasi`) REFERENCES `kantor` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE + CONSTRAINT `aset_ibfk_2` FOREIGN KEY (`id_kantor_lokasi`) REFERENCES `kantor` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT `aset_ibfk_3` FOREIGN KEY (`id_pengguna_penanggung_jawab`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "; $pdo->exec($sql); - echo "Tables `kantor`, `kategori_aset`, and `aset` created or already exist.\n"; + echo "Tables `kantor`, `kategori_aset`, `users`, and `aset` created or already exist.\n"; // --- Seed initial data if tables are empty --- $stmt = $pdo->query("SELECT COUNT(*) FROM `kantor`"); @@ -99,6 +116,16 @@ try { "); echo "Seeded `kategori_aset` table with initial data.\n"; } + + $stmt = $pdo->query("SELECT COUNT(*) FROM `users`"); + if ($stmt->fetchColumn() == 0) { + $hashed_password = password_hash('password', PASSWORD_DEFAULT); + $pdo->exec(" + INSERT INTO `users` (`nama_lengkap`, `email`, `password`, `role`, `id_kantor`) VALUES + ('Super Admin', 'admin@example.com', '{$hashed_password}', 'super_admin', 1); + "); + echo "Seeded `users` table with a super admin user.\n"; + } // Seed a sample asset $stmt = $pdo->query("SELECT COUNT(*) FROM `aset`"); diff --git a/index.php b/index.php index 874001b..cd0260c 100644 --- a/index.php +++ b/index.php @@ -1,3 +1,11 @@ + @@ -28,10 +36,35 @@ -
-
-

Data Aset Perusahaan

-

Selamat datang di sistem manajemen aset terpusat.

+ @@ -146,6 +179,81 @@
+ + +
diff --git a/login.php b/login.php new file mode 100644 index 0000000..32dd4f8 --- /dev/null +++ b/login.php @@ -0,0 +1,64 @@ + + + + + + + Login - Manajemen Aset + + + + + + + + + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..dd0f8e2 --- /dev/null +++ b/logout.php @@ -0,0 +1,21 @@ + + + + + + + Manajemen Kantor - Sistem Manajemen Aset + + + + + +
+
+ + +
+
+

Manajemen Kantor

+ +
+ +
+
+
+ + + + + + + + + + + + +
Nama KantorAlamatTipeAksi
+
+
+
+
+
+
+ + + + + + + + + + diff --git a/users.php b/users.php new file mode 100644 index 0000000..68f095d --- /dev/null +++ b/users.php @@ -0,0 +1,165 @@ +query("SELECT id, nama_kantor FROM kantor ORDER BY nama_kantor"); + $offices = $stmt_offices->fetchAll(PDO::FETCH_ASSOC); +} catch (PDOException $e) { + // Handle error, maybe log it and show a generic message + $offices = []; +} + +?> + + + + + + Manajemen Pengguna - Sistem Manajemen Aset + + + + + +
+ +
+ + +
+
+

Manajemen Pengguna

+ +
+ +
+
+
+ + + + + + + + + + + + + + +
Nama LengkapEmailRoleKantorTgl DibuatAksi
+
+
+
+
+
+
+ + + + + + + + + +