diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..b26d33e 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,117 @@ +:root { + --bg: #f6f7f9; + --surface: #ffffff; + --border: #e5e7eb; + --muted: #6b7280; + --text: #111827; + --primary: #2563eb; + --primary-soft: #dbeafe; + --success: #16a34a; + --warning: #f59e0b; + --danger: #dc2626; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background: var(--bg); + color: var(--text); } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.app-nav .navbar-brand { + letter-spacing: -0.02em; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.card { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: none; } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.card-header { + background: var(--surface); + border-bottom: 1px solid var(--border); } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.stat-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.25rem; } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.stat-label { + color: var(--muted); + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.08em; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; +.stat-value { + font-size: 1.8rem; + font-weight: 600; } .table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; + --bs-table-bg: transparent; } -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; +.table thead th { + font-size: 0.85rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--border); } -.table td { - background: #fff; - padding: 1rem; - border: none; +.table tbody tr { + border-bottom: 1px solid var(--border); } -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; +.badge-soft { + background: var(--primary-soft); + color: var(--primary); + border-radius: 999px; + padding: 0.35rem 0.6rem; + font-weight: 500; } -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; +.form-control, .form-select { + border-radius: var(--radius-sm); + border-color: var(--border); } -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; +.btn-primary { + background: var(--primary); + border-color: var(--primary); + border-radius: var(--radius-sm); } -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); +.btn-outline-primary { + border-radius: var(--radius-sm); } -.header-container { - display: flex; - justify-content: space-between; - align-items: center; +.section-title { + font-size: 1.2rem; + font-weight: 600; } -.header-links { - display: flex; - gap: 1rem; +.small-muted { + color: var(--muted); + font-size: 0.9rem; } -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); +.alert { + border-radius: var(--radius-md); } -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; +.empty-state { + border: 1px dashed var(--border); + border-radius: var(--radius-lg); + padding: 1.5rem; + text-align: center; + color: var(--muted); + background: #fafafa; } - -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; -} - -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; -} - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..941ec32 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,9 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); - - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } - }); + const alerts = document.querySelectorAll('.alert[data-autohide="true"]'); + alerts.forEach((alertEl) => { + setTimeout(() => { + alertEl.classList.add('fade'); + alertEl.addEventListener('transitionend', () => alertEl.remove()); + }, 4000); + }); }); diff --git a/departments.php b/departments.php new file mode 100644 index 0000000..c549c3f --- /dev/null +++ b/departments.php @@ -0,0 +1,114 @@ +prepare("INSERT INTO departments (name, code) VALUES (:name, :code)"); + $stmt->execute([':name' => $name, ':code' => $code]); + header('Location: departments.php?success=1'); + exit; + } +} + +$departments = $pdo->query(" + SELECT d.*, COUNT(e.id) AS total_employee + FROM departments d + LEFT JOIN employees e ON e.dept_id = d.id + GROUP BY d.id + ORDER BY d.name ASC +")->fetchAll(); + +render_header('Data Departemen', 'departments'); +?> + +
+
+

Departemen

+

Struktur organisasi yang digunakan untuk penempatan pegawai.

+
+
+ + +
Departemen berhasil ditambahkan.
+ + + +
+ +
+ + +
+
+
+
+ Tambah Departemen +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ Daftar Departemen + total +
+
+ +
Belum ada departemen. Tambahkan departemen pertama.
+ +
+ + + + + + + + + + + + + + + + + +
NamaKodeJumlah Pegawai
+
+ +
+
+
+
+ + diff --git a/employee_new.php b/employee_new.php new file mode 100644 index 0000000..df0d278 --- /dev/null +++ b/employee_new.php @@ -0,0 +1,140 @@ +query("SELECT id, name FROM departments ORDER BY name ASC")->fetchAll(); +$positions = $pdo->query("SELECT id, name FROM positions ORDER BY name ASC")->fetchAll(); + +$errors = []; +$values = [ + 'employee_code' => '', + 'full_name' => '', + 'email' => '', + 'phone' => '', + 'status' => 'Aktif', + 'employment_type' => 'Kontrak', + 'hire_date' => '', + 'dept_id' => '', + 'position_id' => '', +]; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + foreach ($values as $key => $val) { + $values[$key] = trim($_POST[$key] ?? ''); + } + + if ($values['employee_code'] === '') { + $errors[] = 'Kode pegawai wajib diisi.'; + } + if ($values['full_name'] === '') { + $errors[] = 'Nama lengkap wajib diisi.'; + } + + if (!$errors) { + $stmt = $pdo->prepare(" + INSERT INTO employees (employee_code, full_name, email, phone, status, employment_type, hire_date, dept_id, position_id) + VALUES (:employee_code, :full_name, :email, :phone, :status, :employment_type, :hire_date, :dept_id, :position_id) + "); + $stmt->bindValue(':employee_code', $values['employee_code']); + $stmt->bindValue(':full_name', $values['full_name']); + $stmt->bindValue(':email', $values['email'] !== '' ? $values['email'] : null); + $stmt->bindValue(':phone', $values['phone'] !== '' ? $values['phone'] : null); + $stmt->bindValue(':status', $values['status'] !== '' ? $values['status'] : 'Aktif'); + $stmt->bindValue(':employment_type', $values['employment_type'] !== '' ? $values['employment_type'] : 'Kontrak'); + $stmt->bindValue(':hire_date', $values['hire_date'] !== '' ? $values['hire_date'] : null); + $stmt->bindValue(':dept_id', $values['dept_id'] !== '' ? (int)$values['dept_id'] : null, PDO::PARAM_INT); + $stmt->bindValue(':position_id', $values['position_id'] !== '' ? (int)$values['position_id'] : null, PDO::PARAM_INT); + $stmt->execute(); + + $newId = (int)$pdo->lastInsertId(); + header('Location: employee_view.php?id=' . $newId . '&created=1'); + exit; + } +} + +render_header('Tambah Pegawai', 'employees'); +?> + +
+

Tambah Pegawai

+

Lengkapi profil pegawai baru, status kerja, dan penempatan.

+
+ + + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + diff --git a/employee_view.php b/employee_view.php new file mode 100644 index 0000000..e13ae76 --- /dev/null +++ b/employee_view.php @@ -0,0 +1,113 @@ +prepare(" + SELECT e.*, d.name AS dept_name, d.code AS dept_code, p.name AS position_name, p.level + FROM employees e + LEFT JOIN departments d ON d.id = e.dept_id + LEFT JOIN positions p ON p.id = e.position_id + WHERE e.id = :id +"); +$stmt->execute([':id' => $id]); +$employee = $stmt->fetch(); + +if (!$employee) { + render_header('Pegawai Tidak Ditemukan', 'employees'); + echo '
Pegawai tidak ditemukan.
'; + echo 'Kembali'; + render_footer(); + exit; +} + +$created = isset($_GET['created']); + +render_header('Detail Pegawai', 'employees'); +?> + +
+
+

+

·

+
+ Kembali ke daftar +
+ + +
+ Profil pegawai baru berhasil dibuat. +
+ + +
+
+
+
+ Profil Pegawai +
+
+
+
+
Email
+
+
+
+
Telepon
+
+
+
+
Status Kerja
+
+
+
+
Tipe Kerja
+
+
+
+
Tanggal Masuk
+
+
+
+
Dibuat
+
+
+
+
+
+
+
+
+
+ Penempatan +
+
+
+
Departemen
+
+ + + +
+
+
Jabatan
+
+
+
+
+
+
+
+ Catatan HR +
+
+

Gunakan halaman ini sebagai ringkasan cepat. Modul kontrak, dokumen, dan histori akan ditambahkan pada iterasi berikutnya.

+
+
+
+
+ + diff --git a/employees.php b/employees.php new file mode 100644 index 0000000..abbae47 --- /dev/null +++ b/employees.php @@ -0,0 +1,146 @@ +query("SELECT id, name FROM departments ORDER BY name ASC")->fetchAll(); +$positions = $pdo->query("SELECT id, name FROM positions ORDER BY name ASC")->fetchAll(); + +$q = trim($_GET['q'] ?? ''); +$deptId = $_GET['dept_id'] ?? ''; +$posId = $_GET['position_id'] ?? ''; +$status = $_GET['status'] ?? ''; + +$where = []; +$params = []; +if ($q !== '') { + $where[] = "(e.full_name LIKE :q OR e.employee_code LIKE :q OR e.email LIKE :q)"; + $params[':q'] = '%' . $q . '%'; +} +if ($deptId !== '') { + $where[] = "e.dept_id = :dept_id"; + $params[':dept_id'] = $deptId; +} +if ($posId !== '') { + $where[] = "e.position_id = :position_id"; + $params[':position_id'] = $posId; +} +if ($status !== '') { + $where[] = "e.status = :status"; + $params[':status'] = $status; +} + +$sql = " + SELECT e.*, d.name AS dept_name, p.name AS position_name + FROM employees e + LEFT JOIN departments d ON d.id = e.dept_id + LEFT JOIN positions p ON p.id = e.position_id +"; +if ($where) { + $sql .= " WHERE " . implode(' AND ', $where); +} +$sql .= " ORDER BY e.full_name ASC"; + +$stmt = $pdo->prepare($sql); +$stmt->execute($params); +$employees = $stmt->fetchAll(); + +render_header('Data Pegawai', 'employees'); +?> + +
+
+

Data Pegawai

+

Kelola profil, status kerja, dan penempatan pegawai.

+
+ Tambah Pegawai +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Reset +
+
+
+
+ +
+
+ Daftar Pegawai + hasil +
+
+ +
Belum ada pegawai yang cocok dengan filter ini.
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
NamaKodeDepartemenJabatanStatus
+
+
+
+ Detail +
+
+ +
+
+ + diff --git a/includes/db_init.php b/includes/db_init.php new file mode 100644 index 0000000..20ecade --- /dev/null +++ b/includes/db_init.php @@ -0,0 +1,101 @@ +exec(" + CREATE TABLE IF NOT EXISTS departments ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + code VARCHAR(20) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS positions ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + level VARCHAR(60) DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + + $pdo->exec(" + CREATE TABLE IF NOT EXISTS employees ( + id INT AUTO_INCREMENT PRIMARY KEY, + employee_code VARCHAR(30) NOT NULL, + full_name VARCHAR(160) NOT NULL, + email VARCHAR(160) DEFAULT NULL, + phone VARCHAR(40) DEFAULT NULL, + status VARCHAR(40) NOT NULL DEFAULT 'Aktif', + employment_type VARCHAR(60) DEFAULT 'Kontrak', + hire_date DATE DEFAULT NULL, + dept_id INT DEFAULT NULL, + position_id INT DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_dept (dept_id), + INDEX idx_position (position_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); + } +} + +if (!function_exists('seed_demo_data')) { + function seed_demo_data(): void { + $pdo = db(); + + $deptCount = (int)$pdo->query("SELECT COUNT(*) FROM departments")->fetchColumn(); + if ($deptCount === 0) { + $stmt = $pdo->prepare("INSERT INTO departments (name, code) VALUES (?, ?)"); + $rows = [ + ['HR & People Ops', 'HR'], + ['Finance', 'FIN'], + ['Technology', 'TECH'], + ['Operations', 'OPS'], + ]; + foreach ($rows as $row) { + $stmt->execute($row); + } + } + + $posCount = (int)$pdo->query("SELECT COUNT(*) FROM positions")->fetchColumn(); + if ($posCount === 0) { + $stmt = $pdo->prepare("INSERT INTO positions (name, level) VALUES (?, ?)"); + $rows = [ + ['HR Manager', 'Lead'], + ['Recruiter', 'Staff'], + ['Software Engineer', 'Staff'], + ['Finance Analyst', 'Staff'], + ]; + foreach ($rows as $row) { + $stmt->execute($row); + } + } + + $empCount = (int)$pdo->query("SELECT COUNT(*) FROM employees")->fetchColumn(); + if ($empCount === 0) { + $deptMap = $pdo->query("SELECT id, code FROM departments")->fetchAll(PDO::FETCH_KEY_PAIR); + $posMap = $pdo->query("SELECT id, name FROM positions")->fetchAll(PDO::FETCH_KEY_PAIR); + $stmt = $pdo->prepare(" + INSERT INTO employees (employee_code, full_name, email, phone, status, employment_type, hire_date, dept_id, position_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + "); + $samples = [ + ['EMP-001', 'Anisa Putri', 'anisa@company.id', '0812-100-200', 'Aktif', 'Kontrak', '2024-05-10', $deptMap['HR'], $posMap['HR Manager']], + ['EMP-002', 'Rafi Nugraha', 'rafi@company.id', '0812-301-222', 'Aktif', 'Tetap', '2023-11-02', $deptMap['TECH'], $posMap['Software Engineer']], + ['EMP-003', 'Sinta Rahma', 'sinta@company.id', '0813-403-900', 'Probation', 'Kontrak', '2025-01-08', $deptMap['OPS'], $posMap['Recruiter']], + ['EMP-004', 'Dimas Prakoso', 'dimas@company.id', '0812-112-555', 'Aktif', 'Tetap', '2022-06-19', $deptMap['FIN'], $posMap['Finance Analyst']], + ]; + foreach ($samples as $row) { + $stmt->execute($row); + } + } + } +} + +ensure_schema(); +seed_demo_data(); diff --git a/includes/helpers.php b/includes/helpers.php new file mode 100644 index 0000000..1dfe5ac --- /dev/null +++ b/includes/helpers.php @@ -0,0 +1,13 @@ + + + + + + + <?= e($title) ?> + + + + + + + + + + + + + + +
+ +
+ + + + + + (int)$pdo->query("SELECT COUNT(*) FROM employees")->fetchColumn(), + 'departments' => (int)$pdo->query("SELECT COUNT(*) FROM departments")->fetchColumn(), + 'positions' => (int)$pdo->query("SELECT COUNT(*) FROM positions")->fetchColumn(), +]; + +$deptSummary = $pdo->query(" + SELECT d.name, d.code, COUNT(e.id) AS total + FROM departments d + LEFT JOIN employees e ON e.dept_id = d.id + GROUP BY d.id + ORDER BY total DESC, d.name ASC + LIMIT 6 +")->fetchAll(); + +$posSummary = $pdo->query(" + SELECT p.name, p.level, COUNT(e.id) AS total + FROM positions p + LEFT JOIN employees e ON e.position_id = p.id + GROUP BY p.id + ORDER BY total DESC, p.name ASC + LIMIT 6 +")->fetchAll(); + +$recentEmployees = $pdo->query(" + SELECT e.id, e.full_name, e.employee_code, e.status, d.name AS dept_name, p.name AS position_name + FROM employees e + LEFT JOIN departments d ON d.id = e.dept_id + LEFT JOIN positions p ON p.id = e.position_id + ORDER BY e.created_at DESC + LIMIT 5 +")->fetchAll(); + +render_header('Dashboard HRIS', 'dashboard'); ?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+

Dashboard HRIS

+

Ringkasan data pegawai, departemen, dan jabatan terbaru untuk tim HR dan manajer.

+
+ +
+
+
+
Total Pegawai
+
+
Termasuk status aktif & probation
-
- - - + +
+
+
Departemen
+
+
Struktur organisasi aktif
+
+
+
+
+
Jabatan
+
+
Role yang tersedia
+
+
+ + +
+
+
+
+ Distribusi Departemen + Kelola Departemen +
+
+ +
Belum ada departemen. Tambahkan data departemen pertama Anda.
+ +
+ + + + + + + + + + + + + + + + + +
DepartemenKodeTotal Pegawai
+
+ +
+
+ +
+
+ Distribusi Jabatan + Kelola Jabatan +
+
+ +
Belum ada jabatan. Tambahkan jabatan untuk tim Anda.
+ +
+ + + + + + + + + + + + + + + + + +
JabatanLevelTotal Pegawai
+
+ +
+
+
+ +
+
+
+ Pegawai Terbaru + Lihat Semua +
+
+ +
Belum ada pegawai. Mulai tambah data pegawai baru.
+ +
+ +
+
+
+
·
+
+
+
+ + +
+
+ +
+ +
+
+
+
+ + diff --git a/positions.php b/positions.php new file mode 100644 index 0000000..b09a47e --- /dev/null +++ b/positions.php @@ -0,0 +1,114 @@ +prepare("INSERT INTO positions (name, level) VALUES (:name, :level)"); + $stmt->execute([':name' => $name, ':level' => $level !== '' ? $level : null]); + header('Location: positions.php?success=1'); + exit; + } +} + +$positions = $pdo->query(" + SELECT p.*, COUNT(e.id) AS total_employee + FROM positions p + LEFT JOIN employees e ON e.position_id = p.id + GROUP BY p.id + ORDER BY p.name ASC +")->fetchAll(); + +render_header('Data Jabatan', 'positions'); +?> + +
+
+

Jabatan

+

Master data jabatan dan level dalam organisasi.

+
+
+ + +
Jabatan berhasil ditambahkan.
+ + + +
+ +
+ + +
+
+
+
+ Tambah Jabatan +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ Daftar Jabatan + total +
+
+ +
Belum ada jabatan. Tambahkan jabatan baru.
+ +
+ + + + + + + + + + + + + + + + + +
JabatanLevelJumlah Pegawai
+
+ +
+
+
+
+ +