Compare commits

..

2 Commits

Author SHA1 Message Date
Flatlogic Bot
2be5f009ee dua 2025-10-03 14:19:02 +00:00
Flatlogic Bot
8d0038be8c satu 2025-10-03 13:56:08 +00:00
28 changed files with 2120 additions and 160 deletions

15
.gitignore vendored
View File

@ -1,3 +1,12 @@
node_modules/ # Composer dependencies
*/node_modules/ /vendor/
*/build/
# Environment files
.env
# Log files
*.log
# IDE settings
.idea/
.vscode/

71
api/add_asset.php Normal file
View File

@ -0,0 +1,71 @@
<?php
// api/add_asset.php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
// --- Validation ---
$errors = [];
if (empty($_POST['nama_aset'])) {
$errors[] = 'Nama aset tidak boleh kosong.';
}
if (empty($_POST['id_kategori'])) {
$errors[] = 'Kategori harus dipilih.';
}
if (empty($_POST['id_kantor_lokasi'])) {
$errors[] = 'Lokasi kantor harus dipilih.';
}
if (empty($_POST['tanggal_pembelian'])) {
$errors[] = 'Tanggal pembelian tidak boleh kosong.';
}
if (empty($_POST['harga_pembelian'])) {
$errors[] = 'Harga pembelian tidak boleh kosong.';
}
if (!empty($errors)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => implode(' ', $errors)]);
exit;
}
try {
$pdo = getDbConnection();
// --- Generate Asset Code ---
$stmt = $pdo->prepare("SELECT kode_kategori FROM kategori_aset WHERE id = ?");
$stmt->execute([$_POST['id_kategori']]);
$kode_kategori = $stmt->fetchColumn();
$tahun = date('Y');
$stmt = $pdo->prepare("SELECT COUNT(*) FROM aset WHERE kode_aset LIKE ?");
$stmt->execute([$kode_kategori . '-' . $tahun . '%']);
$nomor_urut = $stmt->fetchColumn() + 1;
$kode_aset = $kode_kategori . '-' . $tahun . '-' . str_pad($nomor_urut, 4, '0', STR_PAD_LEFT);
// --- Insert into Database ---
$sql = "INSERT INTO aset (kode_aset, nama_aset, id_kategori, id_kantor_lokasi, spesifikasi, tanggal_pembelian, harga_pembelian, vendor, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
$kode_aset,
$_POST['nama_aset'],
$_POST['id_kategori'],
$_POST['id_kantor_lokasi'],
$_POST['spesifikasi'] ?? null,
$_POST['tanggal_pembelian'],
$_POST['harga_pembelian'],
$_POST['vendor'] ?? null,
$_POST['status'] ?? 'Tersedia'
]);
echo json_encode(['success' => true, 'message' => 'Aset berhasil ditambahkan.', 'new_asset_id' => $pdo->lastInsertId()]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}
?>

33
api/add_office.php Normal file
View File

@ -0,0 +1,33 @@
<?php
// api/add_office.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

48
api/add_user.php Normal file
View File

@ -0,0 +1,48 @@
<?php
// api/add_user.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

42
api/delete_asset.php Normal file
View File

@ -0,0 +1,42 @@
<?php
require_once '../db/config.php';
session_start();
// Basic security checks
if (!isset($_SESSION['user_id'])) {
http_response_code(403);
echo json_encode(['success' => 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()]);
}

46
api/delete_office.php Normal file
View File

@ -0,0 +1,46 @@
<?php
// api/delete_office.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

42
api/delete_user.php Normal file
View File

@ -0,0 +1,42 @@
<?php
// api/delete_user.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

35
api/get_asset.php Normal file
View File

@ -0,0 +1,35 @@
<?php
require_once '../db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
http_response_code(403);
echo json_encode(['error' => '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()]);
}

34
api/get_assets.php Normal file
View File

@ -0,0 +1,34 @@
<?php
// api/get_assets.php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
try {
$pdo = getDbConnection();
$sql = "
SELECT
a.id,
a.kode_aset,
a.nama_aset,
kc.nama_kategori,
k.nama_kantor,
a.status
FROM aset a
JOIN kategori_aset kc ON a.id_kategori = kc.id
JOIN kantor k ON a.id_kantor_lokasi = k.id
ORDER BY a.created_at DESC
";
$stmt = $pdo->query($sql);
$assets = $stmt->fetchAll();
echo json_encode($assets);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Gagal mengambil data aset: ' . $e->getMessage()]);
}
?>

32
api/get_offices.php Normal file
View File

@ -0,0 +1,32 @@
<?php
// api/get_offices.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

26
api/get_options.php Normal file
View File

@ -0,0 +1,26 @@
<?php
// api/get_options.php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
try {
$pdo = getDbConnection();
$kategori_stmt = $pdo->query("SELECT id, nama_kategori FROM kategori_aset ORDER BY nama_kategori ASC");
$kategori = $kategori_stmt->fetchAll();
$kantor_stmt = $pdo->query("SELECT id, nama_kantor FROM kantor ORDER BY nama_kantor ASC");
$kantor = $kantor_stmt->fetchAll();
echo json_encode([
'kategori' => $kategori,
'kantor' => $kantor
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Gagal mengambil data options: ' . $e->getMessage()]);
}
?>

34
api/get_user.php Normal file
View File

@ -0,0 +1,34 @@
<?php
// api/get_user.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

26
api/get_users.php Normal file
View File

@ -0,0 +1,26 @@
<?php
// api/get_users.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

73
api/update_asset.php Normal file
View File

@ -0,0 +1,73 @@
<?php
require_once '../db/config.php';
session_start();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => 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()]);
}

34
api/update_office.php Normal file
View File

@ -0,0 +1,34 @@
<?php
// api/update_office.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

56
api/update_user.php Normal file
View File

@ -0,0 +1,56 @@
<?php
// api/update_user.php
header('Content-Type: application/json');
require_once '../db/config.php';
session_start();
// Authentication and Authorization check
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'super_admin') {
echo json_encode(['success' => 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()]);
}
?>

95
assets/css/custom.css Normal file
View File

@ -0,0 +1,95 @@
/* assets/css/custom.css */
:root {
--primary-color: #4A90E2;
--secondary-color: #50E3C2;
--background-color: #F4F7F9;
--surface-color: #FFFFFF;
--text-color: #333333;
--heading-font: 'Georgia', serif;
--body-font: 'Inter', sans-serif;
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--body-font);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--heading-font);
}
.app-header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 3rem 0;
text-align: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-weight: 700;
font-size: 2.5rem;
}
.surface-card {
background-color: var(--surface-color);
border: none;
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.card-title {
color: var(--primary-color);
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(74, 144, 226, 0.4);
}
.form-control, .form-select {
border-radius: 0.5rem;
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(74, 144, 226, 0.25);
}
.modal-header {
border-bottom: none;
}
.modal-footer {
border-top: none;
}
.table {
--bs-table-hover-bg: var(--background-color);
}
.status-badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: .75em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.5rem;
}
.status-Tersedia { background-color: #28a745; }
.status-Digunakan { background-color: #17a2b8; }
.status-Perbaikan { background-color: #ffc107; color: #333;}
.status-Dihapuskan { background-color: #dc3545; }

264
assets/js/main.js Normal file
View File

@ -0,0 +1,264 @@
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 ---
/**
* Shows a toast notification.
* @param {string} title - The title of the toast.
* @param {string} body - The body message of the toast.
* @param {string} type - 'success' or 'error'.
*/
function showToast(title, body, type = 'success') {
const toastHeader = notificationToastEl.querySelector('.toast-header');
toastHeader.classList.remove('bg-success', 'text-white', 'bg-danger', 'text-white');
if (type === 'success') {
toastHeader.classList.add('bg-success', 'text-white');
} else if (type === 'error') {
toastHeader.classList.add('bg-danger', 'text-white');
}
document.getElementById('toast-title').textContent = title;
document.getElementById('toast-body').textContent = body;
notificationToast.show();
}
/**
* Fetches data from the API.
* @param {string} endpoint - The API endpoint to fetch from.
* @param {object} options - Optional fetch options.
* @returns {Promise<any>} - The JSON response.
*/
async function fetchApi(endpoint, options = {}) {
try {
const response = await fetch(API_BASE + endpoint, options);
if (!response.ok) {
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', `Terjadi kesalahan: ${error.message}`, 'error');
return null;
}
}
/**
* Populates select dropdowns for both add and edit modals.
*/
async function populateSelectOptions() {
const data = await fetchApi('get_options.php');
if (!data) return;
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' }
];
selects.forEach(s => {
if (s.el) {
s.el.innerHTML = `<option value="">${s.placeholder}</option>`;
s.data.forEach(item => {
s.el.innerHTML += `<option value="${item.id}">${item[s.name]}</option>`;
});
}
});
}
/**
* Fetches assets and renders them in the table.
*/
async function loadAssets() {
loadingIndicator.style.display = 'block';
assetsTableBody.innerHTML = '';
const assets = await fetchApi('get_assets.php');
loadingIndicator.style.display = 'none';
if (assets && assets.length > 0) {
assets.forEach(asset => {
const row = `
<tr>
<td>${asset.kode_aset}</td>
<td>${asset.nama_aset}</td>
<td>${asset.nama_kategori}</td>
<td>${asset.nama_kantor}</td>
<td><span class="status-badge status-${asset.status.toLowerCase()}">${asset.status}</span></td>
<td>
<button class="btn btn-sm btn-outline-primary btn-edit" data-asset-id="${asset.id}" title="Edit">
<i data-feather="edit-2" class="feather-sm"></i>
</button>
<button class="btn btn-sm btn-outline-danger btn-delete" data-asset-id="${asset.id}" title="Hapus">
<i data-feather="trash-2" class="feather-sm"></i>
</button>
</td>
</tr>
`;
assetsTableBody.innerHTML += row;
});
} else if (assets) {
assetsTableBody.innerHTML = '<tr><td colspan="6" class="text-center">Belum ada data aset.</td></tr>';
} else {
assetsTableBody.innerHTML = '<tr><td colspan="6" class="text-center text-danger">Gagal memuat data aset.</td></tr>';
}
feather.replace();
}
/**
* Handles the submission of the 'Add Asset' form.
*/
async function handleAddAssetSubmit(event) {
event.preventDefault();
event.stopPropagation();
if (!addAssetForm.checkValidity()) {
addAssetForm.classList.add('was-validated');
return;
}
const formData = new FormData(addAssetForm);
const submitButton = addAssetForm.querySelector('button[type="submit"]');
submitButton.disabled = true;
submitButton.innerHTML = '''<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Menyimpan...''';
const result = await fetchApi('add_asset.php', { method: 'POST', body: formData });
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';
}
/**
* 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 = '''<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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();
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 => {
if(el.classList.contains('feather-sm')) return;
const iconName = el.getAttribute('data-feather');
el.classList.add(`icon-${iconName}`);
});
});

114
assets/js/offices.js Normal file
View File

@ -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 = `
<td>${office.nama_kantor}</td>
<td>${office.alamat}</td>
<td>${office.tipe_kantor}</td>
<td>
<button class="btn btn-sm btn-outline-secondary edit-btn" data-id="${office.id}"><i data-feather="edit-2" class="feather-sm"></i></button>
<button class="btn btn-sm btn-outline-danger delete-btn" data-id="${office.id}"><i data-feather="trash-2" class="feather-sm"></i></button>
</td>
`;
});
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();
});

128
assets/js/users.js Normal file
View File

@ -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 = `
<td>${user.nama_lengkap}</td>
<td>${user.email}</td>
<td>${user.role.replace('_', ' ')}</td>
<td>${user.nama_kantor || 'N/A'}</td>
<td>${new Date(user.created_at).toLocaleDateString()}</td>
<td>
<button class="btn btn-sm btn-outline-secondary edit-btn" data-id="${user.id}"><i data-feather="edit-2" class="feather-sm"></i></button>
<button class="btn btn-sm btn-outline-danger delete-btn" data-id="${user.id}"><i data-feather="trash-2" class="feather-sm"></i></button>
</td>
`;
});
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();
});

46
auth.php Normal file
View File

@ -0,0 +1,46 @@
<?php
session_start();
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($email) || empty($password)) {
$_SESSION['login_error'] = 'Email dan password harus diisi.';
header('Location: login.php');
exit();
}
try {
$pdo = db();
$stmt = $pdo->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();
}

View File

@ -1,17 +1,38 @@
<?php <?php
// Generated by setup_mariadb_project.sh — edit as needed. // db/config.php
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_30908');
define('DB_USER', 'app_30908');
define('DB_PASS', '98b730aa-be6c-479d-a47d-e5e7abc49229');
function db() { // --- Database Credentials ---
static $pdo; // In a real environment, use environment variables.
if (!$pdo) { define('DB_HOST', '127.0.0.1');
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ define('DB_NAME', 'asset_management');
define('DB_USER', 'asset_admin');
define('DB_PASS', 'password');
define('DB_CHARSET', 'utf8mb4');
/**
* Establishes a PDO database connection.
*
* @return PDO The PDO database connection object.
* @throws PDOException if the connection fails.
*/
function getDbConnection() {
static $pdo = null;
if ($pdo === null) {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]); PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (\PDOException $e) {
// For development, it's okay to see the error.
// In production, log this and show a generic error message.
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
} }
return $pdo; return $pdo;
} }
?>

146
db/setup.php Normal file
View File

@ -0,0 +1,146 @@
<?php
// db/setup.php
// This script initializes the database and tables.
// Run it once from the command line: php db/setup.php
// --- Database Credentials (temporary for setup) ---
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'asset_management');
define('DB_USER', 'asset_admin');
define('DB_PASS', 'password');
define('DB_CHARSET', 'utf8mb4');
header('Content-Type: text/plain');
try {
// Connect without specifying a database to create it
$dsn_init = "mysql:host=" . DB_HOST . ";charset=" . DB_CHARSET;
$pdo_init = new PDO($dsn_init, DB_USER, DB_PASS);
$pdo_init->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create the database if it doesn't exist
$pdo_init->exec("CREATE DATABASE IF NOT EXISTS `" . DB_NAME . "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;");
echo "Database '" . DB_NAME . "' created or already exists.\n";
// Now connect to the newly created database
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// SQL to create tables
$sql = "
CREATE TABLE IF NOT EXISTS `kantor` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama_kantor` varchar(255) NOT NULL,
`alamat` text DEFAULT NULL,
`tipe_kantor` enum('pusat','cabang') NOT NULL DEFAULT 'cabang',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `kategori_aset` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nama_kategori` varchar(255) NOT NULL,
`kode_kategori` varchar(10) NOT 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 `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,
`nama_aset` varchar(255) NOT NULL,
`id_kategori` int(11) NOT NULL,
`id_kantor_lokasi` int(11) NOT NULL,
`spesifikasi` text DEFAULT NULL,
`tanggal_pembelian` date NOT NULL,
`harga_pembelian` decimal(15,2) NOT NULL,
`vendor` varchar(255) DEFAULT NULL,
`status` enum('Tersedia','Digunakan','Perbaikan','Dihapuskan') NOT NULL DEFAULT 'Tersedia',
`id_pengguna_penanggung_jawab` 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 `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_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`, `users`, and `aset` created or already exist.\n";
// --- Seed initial data if tables are empty ---
$stmt = $pdo->query("SELECT COUNT(*) FROM `kantor`");
if ($stmt->fetchColumn() == 0) {
$pdo->exec("
INSERT INTO `kantor` (`nama_kantor`, `alamat`, `tipe_kantor`) VALUES
('Kantor Pusat Jakarta', 'Jl. Jenderal Sudirman Kav. 52-53, Jakarta Selatan', 'pusat'),
('Cabang Surabaya', 'Jl. Basuki Rahmat No. 129, Surabaya', 'cabang'),
('Cabang Bandung', 'Jl. Asia Afrika No. 1, Bandung', 'cabang');
");
echo "Seeded `kantor` table with initial data.\n";
}
$stmt = $pdo->query("SELECT COUNT(*) FROM `kategori_aset`");
if ($stmt->fetchColumn() == 0) {
$pdo->exec("
INSERT INTO `kategori_aset` (`nama_kategori`, `kode_kategori`) VALUES
('Elektronik', 'ELK'),
('Furnitur', 'FNT'),
('Kendaraan', 'KDR'),
('Peralatan Kantor', 'PKR');
");
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`");
if ($stmt->fetchColumn() == 0) {
$pdo->exec("
INSERT INTO `aset` (`kode_aset`, `nama_aset`, `id_kategori`, `id_kantor_lokasi`, `spesifikasi`, `tanggal_pembelian`, `harga_pembelian`, `vendor`, `status`) VALUES
('ELK-2025-0001', 'Laptop Dell XPS 15', 1, 1, 'CPU i9, 32GB RAM, 1TB SSD', '2025-01-15', 45000000.00, 'Dell Indonesia', 'Digunakan');
");
echo "Seeded `aset` table with a sample asset.\n";
}
echo "\nDatabase setup completed successfully!\n";
} catch (PDOException $e) {
die("Database setup failed: " . $e->getMessage() . "\n");
}
?>

402
index.php
View File

@ -1,150 +1,276 @@
<?php <?php
declare(strict_types=1); session_start();
@ini_set('display_errors', '1'); // If the user is not logged in, redirect to the login page.
@error_reporting(E_ALL); if (!isset($_SESSION['user_id'])) {
@date_default_timezone_set('UTC'); header('Location: login.php');
exit();
$phpVersion = PHP_VERSION; }
$now = date('Y-m-d H:i:s');
?> ?>
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="id">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Style</title>
<?php <title>Data Aset Andi Offset</title>
// Read project preview data from environment <meta name="description" content="Aplikasi Web Pendataan Aset Perusahaan: Kelola dan lacak aset perusahaan secara terpusat di semua kantor.">
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <meta name="keywords" content="manajemen aset, pendataan aset, inventaris perusahaan, software aset, aplikasi aset, tracking aset, kelola aset kantor, sistem informasi aset, database aset, andi offset">
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?> <!-- Open Graph / Facebook -->
<?php if ($projectDescription): ?> <meta property="og:type" content="website">
<!-- Meta description --> <meta property="og:title" content="Data Aset Andi Offset">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <meta property="og:description" content="Aplikasi Web Pendataan Aset Perusahaan: Kelola dan lacak aset perusahaan secara terpusat di semua kantor.">
<!-- Open Graph meta tags --> <meta property="og:image" content="https://project-screens.s3.amazonaws.com/screenshots/34627/app-hero-20251003-135016.png">
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags --> <!-- Twitter -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" /> <meta name="twitter:card" content="summary_large_image">
<?php endif; ?> <meta name="twitter:image" content="https://project-screens.s3.amazonaws.com/screenshots/34627/app-hero-20251003-135016.png">
<?php if ($projectImageUrl): ?>
<!-- Open Graph image --> <!-- Styles -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Georgia:wght@700&display=swap" rel="stylesheet">
<style> <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main>
<div class="card"> <header class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<h1>Analyzing your requirements and generating your website…</h1> <div class="container-fluid">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <a class="navbar-brand" href="index.php">Manajemen Aset</a>
<span class="sr-only">Loading…</span> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<?php if (isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'super_admin'): ?>
<li class="nav-item">
<a class="nav-link active" href="index.php">Aset</a>
</li>
<li class="nav-item">
<a class="nav-link" href="users.php">Pengguna</a>
</li>
<li class="nav-item">
<a class="nav-link" href="offices.php">Kantor</a>
</li>
<?php endif; ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i data-feather="user" class="me-1"></i> <?php echo htmlspecialchars($_SESSION['user_name']); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</header>
<main class="container my-5">
<div class="card surface-card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="card-title mb-0">Daftar Aset</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addAssetModal">
<i data-feather="plus" class="me-2"></i>Tambah Aset Baru
</button>
</div>
<!-- TODO: Filter/Search UI -->
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Kode Aset</th>
<th>Nama Aset</th>
<th>Kategori</th>
<th>Lokasi</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="assets-table-body">
<!-- Asset rows will be injected here by JavaScript -->
</tbody>
</table>
</div>
<div id="loading-indicator" class="text-center my-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> </main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <!-- Add Asset Modal -->
</footer> <div class="modal fade" id="addAssetModal" tabindex="-1" aria-labelledby="addAssetModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addAssetModalLabel">Tambah Aset Baru</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addAssetForm" class="needs-validation" novalidate>
<div class="row">
<div class="col-md-12 mb-3">
<label for="nama_aset" class="form-label">Nama Aset</label>
<input type="text" class="form-control" id="nama_aset" name="nama_aset" required>
<div class="invalid-feedback">Nama aset tidak boleh kosong.</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="id_kategori" class="form-label">Kategori</label>
<select class="form-select" id="id_kategori" name="id_kategori" required>
<!-- Options will be populated by JS -->
</select>
<div class="invalid-feedback">Silakan pilih kategori.</div>
</div>
<div class="col-md-6 mb-3">
<label for="id_kantor_lokasi" class="form-label">Lokasi Kantor</label>
<select class="form-select" id="id_kantor_lokasi" name="id_kantor_lokasi" required>
<!-- Options will be populated by JS -->
</select>
<div class="invalid-feedback">Silakan pilih lokasi.</div>
</div>
</div>
<div class="mb-3">
<label for="spesifikasi" class="form-label">Spesifikasi</label>
<textarea class="form-control" id="spesifikasi" name="spesifikasi" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="tanggal_pembelian" class="form-label">Tanggal Pembelian</label>
<input type="date" class="form-control" id="tanggal_pembelian" name="tanggal_pembelian" required>
<div class="invalid-feedback">Tanggal pembelian tidak boleh kosong.</div>
</div>
<div class="col-md-6 mb-3">
<label for="harga_pembelian" class="form-label">Harga Pembelian (Rp)</label>
<input type="number" class="form-control" id="harga_pembelian" name="harga_pembelian" step="0.01" required>
<div class="invalid-feedback">Harga pembelian tidak boleh kosong.</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="vendor" class="form-label">Vendor</label>
<input type="text" class="form-control" id="vendor" name="vendor">
</div>
<div class="col-md-6 mb-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status" required>
<option value="Tersedia">Tersedia</option>
<option value="Digunakan">Digunakan</option>
<option value="Perbaikan">Perbaikan</option>
<option value="Dihapuskan">Dihapuskan</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan Aset</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit Asset Modal -->
<div class="modal fade" id="editAssetModal" tabindex="-1" aria-labelledby="editAssetModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editAssetModalLabel">Edit Aset</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editAssetForm" class="needs-validation" novalidate>
<input type="hidden" id="edit_id" name="id">
<div class="row">
<div class="col-md-12 mb-3">
<label for="edit_nama_aset" class="form-label">Nama Aset</label>
<input type="text" class="form-control" id="edit_nama_aset" name="nama_aset" required>
<div class="invalid-feedback">Nama aset tidak boleh kosong.</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_id_kategori" class="form-label">Kategori</label>
<select class="form-select" id="edit_id_kategori" name="id_kategori" required>
<!-- Options will be populated by JS -->
</select>
<div class="invalid-feedback">Silakan pilih kategori.</div>
</div>
<div class="col-md-6 mb-3">
<label for="edit_id_kantor_lokasi" class="form-label">Lokasi Kantor</label>
<select class="form-select" id="edit_id_kantor_lokasi" name="id_kantor_lokasi" required>
<!-- Options will be populated by JS -->
</select>
<div class="invalid-feedback">Silakan pilih lokasi.</div>
</div>
</div>
<div class="mb-3">
<label for="edit_spesifikasi" class="form-label">Spesifikasi</label>
<textarea class="form-control" id="edit_spesifikasi" name="spesifikasi" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_tanggal_pembelian" class="form-label">Tanggal Pembelian</label>
<input type="date" class="form-control" id="edit_tanggal_pembelian" name="tanggal_pembelian" required>
<div class="invalid-feedback">Tanggal pembelian tidak boleh kosong.</div>
</div>
<div class="col-md-6 mb-3">
<label for="edit_harga_pembelian" class="form-label">Harga Pembelian (Rp)</label>
<input type="number" class="form-control" id="edit_harga_pembelian" name="harga_pembelian" step="0.01" required>
<div class="invalid-feedback">Harga pembelian tidak boleh kosong.</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="edit_vendor" class="form-label">Vendor</label>
<input type="text" class="form-control" id="edit_vendor" name="vendor">
</div>
<div class="col-md-6 mb-3">
<label for="edit_status" class="form-label">Status</label>
<select class="form-select" id="edit_status" name="status" required>
<option value="Tersedia">Tersedia</option>
<option value="Digunakan">Digunakan</option>
<option value="Perbaikan">Perbaikan</option>
<option value="Dihapuskan">Dihapuskan</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Toast container -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="notificationToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto" id="toast-title"></strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="toast-body">
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body> </body>
</html> </html>

64
login.php Normal file
View File

@ -0,0 +1,64 @@
<?php
session_start();
if (isset($_SESSION['user_id'])) {
header('Location: index.php');
exit();
}
$error = '';
if (isset($_SESSION['login_error'])) {
$error = $_SESSION['login_error'];
unset($_SESSION['login_error']);
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Manajemen Aset</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
<style>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #F4F7F9;
}
.login-card {
max-width: 400px;
width: 100%;
}
</style>
</head>
<body>
<div class="card login-card shadow-sm">
<div class="card-body p-5">
<h3 class="card-title text-center mb-4">Login Sistem</h3>
<?php if ($error): ?>
<div class="alert alert-danger" role="alert">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form action="auth.php" method="POST">
<div class="mb-3">
<label for="email" class="form-label">Alamat Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

21
logout.php Normal file
View File

@ -0,0 +1,21 @@
<?php
session_start();
// Unset all of the session variables
$_SESSION = [];
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Finally, destroy the session.
session_destroy();
header('Location: login.php');
exit();

129
offices.php Normal file
View File

@ -0,0 +1,129 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
if ($_SESSION['user_role'] !== 'super_admin') {
header('Location: index.php?error=unauthorized');
exit;
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manajemen Kantor - Sistem Manajemen Aset</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.css" rel="stylesheet">
<link href="assets/css/custom.css" rel="stylesheet">
</head>
<body>
<div class="d-flex">
<div class="main-content flex-grow-1">
<header class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Manajemen Aset</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<?php if ($_SESSION['user_role'] === 'super_admin'): ?>
<li class="nav-item">
<a class="nav-link" href="index.php">Aset</a>
</li>
<li class="nav-item">
<a class="nav-link" href="users.php">Pengguna</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="offices.php">Kantor</a>
</li>
<?php endif; ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i data-feather="user" class="me-1"></i> <?php echo htmlspecialchars($_SESSION['user_name']); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</header>
<main class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Manajemen Kantor</h1>
<button class="btn btn-primary" id="btn-add-office"><i data-feather="plus" class="me-1"></i> Tambah Kantor Baru</button>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="offices-table">
<thead class="table-light">
<tr>
<th>Nama Kantor</th>
<th>Alamat</th>
<th>Tipe</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<!-- Office data will be loaded here by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Add/Edit Office Modal -->
<div class="modal fade" id="office-modal" tabindex="-1" aria-labelledby="office-modal-label" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="office-modal-label">Tambah Kantor Baru</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="office-form">
<input type="hidden" id="office-id" name="id">
<div class="mb-3">
<label for="nama_kantor" class="form-label">Nama Kantor</label>
<input type="text" class="form-control" id="nama_kantor" name="nama_kantor" required>
</div>
<div class="mb-3">
<label for="alamat" class="form-label">Alamat</label>
<textarea class="form-control" id="alamat" name="alamat" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="tipe_kantor" class="form-label">Tipe Kantor</label>
<select class="form-select" id="tipe_kantor" name="tipe_kantor" required>
<option value="pusat">Pusat</option>
<option value="cabang" selected>Cabang</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script>
feather.replace();
</script>
<script src="assets/js/offices.js"></script>
</body>
</html>

165
users.php Normal file
View File

@ -0,0 +1,165 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
if ($_SESSION['user_role'] !== 'super_admin') {
// Optionally, redirect to a "not authorized" page or back to index
header('Location: index.php?error=unauthorized');
exit;
}
// Include database configuration
require_once 'db/config.php';
// Fetch options for form selects (offices)
try {
$stmt_offices = db()->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 = [];
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manajemen Pengguna - Sistem Manajemen Aset</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.css" rel="stylesheet">
<link href="assets/css/custom.css" rel="stylesheet">
</head>
<body>
<div class="d-flex">
<!-- Sidebar can be added here if needed -->
<div class="main-content flex-grow-1">
<header class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Manajemen Aset</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<?php if ($_SESSION['user_role'] === 'super_admin'): ?>
<li class="nav-item">
<a class="nav-link" href="index.php">Aset</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="users.php">Pengguna</a>
</li>
<li class="nav-item">
<a class="nav-link" href="offices.php">Kantor</a>
</li>
<?php endif; ?>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i data-feather="user" class="me-1"></i> <?php echo htmlspecialchars($_SESSION['user_name']); ?>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</header>
<main class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Manajemen Pengguna</h1>
<button class="btn btn-primary" id="btn-add-user"><i data-feather="plus" class="me-1"></i> Tambah Pengguna Baru</button>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover" id="users-table">
<thead class="table-light">
<tr>
<th>Nama Lengkap</th>
<th>Email</th>
<th>Role</th>
<th>Kantor</th>
<th>Tgl Dibuat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<!-- User data will be loaded here by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Add/Edit User Modal -->
<div class="modal fade" id="user-modal" tabindex="-1" aria-labelledby="user-modal-label" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="user-modal-label">Tambah Pengguna Baru</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="user-form">
<input type="hidden" id="user-id" name="id">
<div class="row">
<div class="col-md-6 mb-3">
<label for="nama_lengkap" class="form-label">Nama Lengkap</label>
<input type="text" class="form-control" id="nama_lengkap" name="nama_lengkap" required>
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password">
<small class="form-text text-muted">Kosongkan jika tidak ingin mengubah password.</small>
</div>
<div class="col-md-6 mb-3">
<label for="role" class="form-label">Role</label>
<select class="form-select" id="role" name="role" required>
<option value="super_admin">Super Admin</option>
<option value="admin_cabang">Admin Cabang</option>
<option value="pegawai" selected>Pegawai</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="id_kantor" class="form-label">Kantor</label>
<select class="form-select" id="id_kantor" name="id_kantor">
<option value="">Tidak Ditugaskan</option>
<?php foreach ($offices as $office): ?>
<option value="<?php echo $office['id']; ?>"><?php echo htmlspecialchars($office['nama_kantor']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script>
feather.replace();
</script>
<script src="assets/js/users.js"></script>
</body>
</html>