diff --git a/api/students.php b/api/students.php
new file mode 100644
index 0000000..d0bde94
--- /dev/null
+++ b/api/students.php
@@ -0,0 +1,33 @@
+ 200) {
+ $limit = 50;
+}
+
+$stmt = $db->prepare("
+ SELECT s.id, s.student_code, s.full_name, s.status, s.gender,
+ c.name AS class_name, c.grade_level
+ FROM students s
+ LEFT JOIN classes c ON c.id = s.class_id
+ ORDER BY s.created_at DESC
+ LIMIT :limit
+");
+$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+$stmt->execute();
+$students = $stmt->fetchAll();
+
+echo json_encode([
+ 'data' => $students,
+ 'count' => count($students),
+]);
diff --git a/assets/css/custom.css b/assets/css/custom.css
index 789132e..defdbb7 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -1,403 +1,150 @@
-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;
+:root {
+ --bg: #f6f7f9;
+ --surface: #ffffff;
+ --border: #e3e6ea;
+ --text: #0f172a;
+ --muted: #6b7280;
+ --accent: #1d4ed8;
+ --accent-soft: rgba(29, 78, 216, 0.08);
}
-.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;
}
-@keyframes gradient {
- 0% {
- background-position: 0% 50%;
- }
- 50% {
- background-position: 100% 50%;
- }
- 100% {
- background-position: 0% 50%;
- }
-}
-
-.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;
-}
-
-.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;
-}
-
-.chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 1.5rem;
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
-}
-
-/* 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 {
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
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;
+ line-height: 1.5;
}
-.header-link:hover {
- background: rgba(0, 0, 0, 0.4);
+a {
+ color: var(--accent);
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;
+a:hover {
+ color: #153eae;
}
-.admin-container h1 {
- margin-top: 0;
- color: #212529;
- font-weight: 800;
+.app-shell {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
}
-.table {
- width: 100%;
- border-collapse: separate;
- border-spacing: 0 8px;
- margin-top: 1.5rem;
+.navbar {
+ border-bottom: 1px solid var(--border);
+ background: var(--surface);
}
-.table th {
- background: transparent;
- border: none;
- padding: 1rem;
- color: #6c757d;
+.navbar-brand {
font-weight: 600;
+ letter-spacing: -0.2px;
+}
+
+.badge-role {
+ background: var(--accent-soft);
+ color: var(--accent);
+ font-weight: 600;
+ border: 1px solid rgba(29, 78, 216, 0.2);
+}
+
+.hero {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 24px;
+}
+
+.module-card,
+.card-soft {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ box-shadow: 0 2px 12px rgba(15, 23, 42, 0.04);
+}
+
+.module-card .card-body {
+ padding: 18px;
+}
+
+.module-title {
+ font-size: 15px;
+ font-weight: 600;
+}
+
+.module-meta {
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.table thead th {
+ font-weight: 600;
+ font-size: 12px;
text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 1px;
+ letter-spacing: 0.4px;
+ color: var(--muted);
+ border-bottom: 1px solid var(--border);
}
.table td {
- background: #fff;
- padding: 1rem;
- border: none;
+ vertical-align: middle;
}
-.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;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
- font-size: 0.9rem;
-}
-
-.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;
-}
-
-.form-control:focus {
- outline: none;
- border-color: #23a6d5;
- box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
-}
-
-.header-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.header-links {
- display: flex;
- gap: 1rem;
-}
-
-.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);
-}
-
-.admin-card h3 {
- margin-top: 0;
- margin-bottom: 1.5rem;
- font-weight: 700;
-}
-
-.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);
+.form-control,
+.form-select {
border-radius: 8px;
- padding: 8px;
+ border-color: var(--border);
+ font-size: 14px;
}
-.history-table-ai {
- width: 50%;
- background: rgba(255, 255, 255, 0.5);
+.form-control:focus,
+.form-select:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.12);
+}
+
+.btn-primary {
+ background: var(--accent);
+ border-color: var(--accent);
border-radius: 8px;
- padding: 8px;
+ font-weight: 600;
}
-.no-messages {
- text-align: center;
- color: #777;
-}
\ No newline at end of file
+.btn-outline-secondary {
+ border-radius: 8px;
+}
+
+.section-title {
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 12px;
+}
+
+.footer {
+ margin-top: auto;
+ padding: 24px 0;
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.toast {
+ border-radius: 10px;
+ border: 1px solid var(--border);
+ box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
+}
+
+.search-input {
+ max-width: 280px;
+}
+
+.empty-state {
+ border: 1px dashed var(--border);
+ border-radius: 10px;
+ padding: 18px;
+ color: var(--muted);
+ background: #fafbfc;
+}
diff --git a/assets/js/main.js b/assets/js/main.js
index d349598..6e3f248 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,39 +1,31 @@
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 toastEl = document.getElementById('liveToast');
+ if (toastEl) {
+ const toast = new bootstrap.Toast(toastEl);
+ if (toastEl.dataset.autoshow === '1') {
+ toast.show();
}
- });
+ }
+
+ const roleSelect = document.getElementById('roleSelect');
+ if (roleSelect) {
+ roleSelect.addEventListener('change', () => {
+ const role = roleSelect.value;
+ const url = new URL(window.location.href);
+ url.searchParams.set('role', role);
+ window.location.href = url.toString();
+ });
+ }
+
+ const searchInput = document.getElementById('studentSearch');
+ const tableRows = document.querySelectorAll('[data-student-row]');
+ if (searchInput) {
+ searchInput.addEventListener('input', () => {
+ const query = searchInput.value.toLowerCase();
+ tableRows.forEach((row) => {
+ const hay = row.dataset.studentRow || '';
+ row.style.display = hay.includes(query) ? '' : 'none';
+ });
+ });
+ }
});
diff --git a/attendance.php b/attendance.php
new file mode 100644
index 0000000..641db1d
--- /dev/null
+++ b/attendance.php
@@ -0,0 +1,44 @@
+query("SELECT * FROM classes ORDER BY name ASC")->fetchAll();
+?>
+
+
+
+
+
+ Absensi Harian — Dashboard Kelas
+
+
+
+
+
+
+
Pilih Kelas untuk Absensi
+
+
+
+
+
= htmlspecialchars($class['name']) ?>
+
Guru: = htmlspecialchars($class['homeroom_teacher'] ?? 'Belum ditentukan') ?>
+
Buka Absensi
+
+
+
+
+
+
+
diff --git a/attendance_detail.php b/attendance_detail.php
new file mode 100644
index 0000000..99aff35
--- /dev/null
+++ b/attendance_detail.php
@@ -0,0 +1,71 @@
+ $status) {
+ $stmt = $db->prepare("INSERT INTO attendance (student_id, class_id, status, attendance_date) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?");
+ $stmt->execute([(int)$student_id, $class_id, $status, $date, $status]);
+ }
+ $message = "Absensi berhasil disimpan untuk tanggal $date.";
+}
+
+$class = $db->prepare("SELECT * FROM classes WHERE id = ?");
+$class->execute([$class_id]);
+$classData = $class->fetch();
+
+$students = $db->prepare("SELECT s.id, s.full_name, a.status FROM students s LEFT JOIN attendance a ON s.id = a.student_id AND a.attendance_date = ? WHERE s.class_id = ?");
+$students->execute([$date, $class_id]);
+$studentList = $students->fetchAll();
+?>
+
+
+
+
+
+ Absensi — = htmlspecialchars($classData['name'] ?? 'Kelas') ?>
+
+
+
+
+
+
+
diff --git a/attendance_recap.php b/attendance_recap.php
new file mode 100644
index 0000000..b633999
--- /dev/null
+++ b/attendance_recap.php
@@ -0,0 +1,48 @@
+prepare("SELECT c.name as class_name,
+ SUM(CASE WHEN a.status = 'hadir' THEN 1 ELSE 0 END) as hadir,
+ SUM(CASE WHEN a.status = 'sakit' THEN 1 ELSE 0 END) as sakit,
+ SUM(CASE WHEN a.status = 'izin' THEN 1 ELSE 0 END) as izin,
+ SUM(CASE WHEN a.status = 'alpa' THEN 1 ELSE 0 END) as alpa
+ FROM classes c
+ LEFT JOIN attendance a ON c.id = a.class_id AND a.attendance_date = ?
+ GROUP BY c.id");
+$recap->execute([$date]);
+$data = $recap->fetchAll();
+?>
+
+
+
+
+
+ Rekap Absensi — = htmlspecialchars($date) ?>
+
+
+
+
+ Rekap Absensi: = htmlspecialchars($date) ?>
+
+
+ | Kelas | Hadir | Sakit | Izin | Alpa |
+
+
+
+
+ | = htmlspecialchars($row['class_name']) ?> |
+ = (int)$row['hadir'] ?> |
+ = (int)$row['sakit'] ?> |
+ = (int)$row['izin'] ?> |
+ = (int)$row['alpa'] ?> |
+
+
+
+
+
+
diff --git a/db/migrations/001_school_core.sql b/db/migrations/001_school_core.sql
new file mode 100644
index 0000000..390c0ef
--- /dev/null
+++ b/db/migrations/001_school_core.sql
@@ -0,0 +1,22 @@
+-- Core schema for School Management MVP slice (idempotent)
+CREATE TABLE IF NOT EXISTS classes (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(120) NOT NULL,
+ grade_level VARCHAR(20) NOT NULL,
+ homeroom_teacher VARCHAR(120) DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS students (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ student_code VARCHAR(30) NOT NULL UNIQUE,
+ full_name VARCHAR(160) NOT NULL,
+ gender VARCHAR(12) DEFAULT NULL,
+ class_id INT DEFAULT NULL,
+ guardian_name VARCHAR(160) DEFAULT NULL,
+ status VARCHAR(20) NOT NULL DEFAULT 'aktif',
+ notes TEXT DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_students_class (class_id),
+ INDEX idx_students_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/db/migrations/002_attendance_module.sql b/db/migrations/002_attendance_module.sql
new file mode 100644
index 0000000..903de42
--- /dev/null
+++ b/db/migrations/002_attendance_module.sql
@@ -0,0 +1,12 @@
+-- Attendance migration
+CREATE TABLE IF NOT EXISTS attendance (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ student_id INT NOT NULL,
+ class_id INT NOT NULL,
+ status ENUM('hadir', 'sakit', 'izin', 'alpa') NOT NULL DEFAULT 'alpa',
+ attendance_date DATE NOT NULL,
+ recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
+ FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE CASCADE,
+ UNIQUE KEY unique_attendance (student_id, attendance_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/includes/db_init.php b/includes/db_init.php
new file mode 100644
index 0000000..7b1d223
--- /dev/null
+++ b/includes/db_init.php
@@ -0,0 +1,51 @@
+exec("
+ CREATE TABLE IF NOT EXISTS classes (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(120) NOT NULL,
+ grade_level VARCHAR(20) NOT NULL,
+ homeroom_teacher VARCHAR(120) DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+ ");
+
+ $db->exec("
+ CREATE TABLE IF NOT EXISTS students (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ student_code VARCHAR(30) NOT NULL UNIQUE,
+ full_name VARCHAR(160) NOT NULL,
+ gender VARCHAR(12) DEFAULT NULL,
+ class_id INT DEFAULT NULL,
+ guardian_name VARCHAR(160) DEFAULT NULL,
+ status VARCHAR(20) NOT NULL DEFAULT 'aktif',
+ notes TEXT DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_students_class (class_id),
+ INDEX idx_students_status (status)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+ ");
+}
+
+function seed_classes(PDO $db): void {
+ $count = (int)$db->query("SELECT COUNT(*) FROM classes")->fetchColumn();
+ if ($count > 0) {
+ return;
+ }
+ $defaults = [
+ ['X IPA 1', '10', 'Ibu Fitri'],
+ ['X IPA 2', '10', 'Bapak Dedi'],
+ ['XI IPS 1', '11', 'Ibu Rina'],
+ ['XII IPA 1', '12', 'Bapak Arif'],
+ ];
+ $stmt = $db->prepare("INSERT INTO classes (name, grade_level, homeroom_teacher) VALUES (:name, :grade, :teacher)");
+ foreach ($defaults as $row) {
+ $stmt->execute([
+ ':name' => $row[0],
+ ':grade' => $row[1],
+ ':teacher' => $row[2],
+ ]);
+ }
+}
diff --git a/index.php b/index.php
index 7205f3d..2b95821 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,175 @@
'Admin Sekolah',
+ 'guru' => 'Guru',
+ 'orang_tua' => 'Orang Tua',
+ 'siswa' => 'Siswa',
+ 'bendahara' => 'Bendahara',
+];
+
+$navItems = [
+ 'admin' => ['Dashboard', 'Siswa', 'Kelas', 'Absensi', 'Ujian', 'Keuangan', 'Laporan'],
+ 'guru' => ['Dashboard', 'Jadwal', 'Kelas', 'Absensi', 'Tugas', 'Nilai'],
+ 'orang_tua' => ['Dashboard', 'Perkembangan', 'Tagihan', 'Pengumuman'],
+ 'siswa' => ['Dashboard', 'Jadwal', 'Tugas', 'Ujian', 'Nilai'],
+ 'bendahara' => ['Dashboard', 'Tagihan', 'Pembayaran', 'Rekonsiliasi'],
+];
+
+$studentCount = (int)$db->query("SELECT COUNT(*) FROM students")->fetchColumn();
+$classCount = (int)$db->query("SELECT COUNT(*) FROM classes")->fetchColumn();
+$latestStudent = $db->query("SELECT full_name, created_at FROM students ORDER BY created_at DESC LIMIT 1")->fetch();
+
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
+$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+$projectName = $_SERVER['PROJECT_NAME'] ?? 'School Management System';
?>
-
+
- New Style
-
+ = htmlspecialchars($projectName) ?> — Dashboard
-
-
-
+
-
-
-
-
-
+
+
+
-
-
-
Analyzing your requirements and generating your website…
-
-
Loading…
+
+
+
+
+
+
+
+
Dashboard Operasional Sekolah
+
Pantau data siswa, kelas, dan modul harian dari satu panel. Gunakan menu sesuai hak akses role.
+
+
+
+
+
+
+
+
+
Ringkasan Data
+
+
+
Total Siswa
+
= $studentCount ?>
+
+
+
Total Kelas
+
= $classCount ?>
+
+
+
+ Terakhir ditambah: = $latestStudent ? htmlspecialchars($latestStudent['full_name']) . ' (' . htmlspecialchars($latestStudent['created_at']) . ')' : 'Belum ada data' ?>
+
+
+
+
+
+
Menu Cepat (= htmlspecialchars($roleLabels[$role]) ?>)
+
+
+ = htmlspecialchars($item) ?>
+
+
+
Menu ditampilkan sesuai RBAC. Modul yang belum tersedia akan segera ditambahkan.
+
+
+
+
+
+
+
+
+
Master Data Siswa
+
Registrasi, kelas, wali, status
+
Input data siswa baru, lihat daftar, dan detail profil.
+
Buka Modul
+
+
+
+
+
+
+
Absensi Harian
+
QR/manual + rekap
+
Tandai hadir/sakit/izin dan lihat ringkasan kelas.
+
Buka Modul
+
Lihat Rekap
+
+
+
+
+
+
+
Keuangan & Tagihan
+
Pembayaran + verifikasi
+
Kelola tagihan SPP dan konfirmasi pembayaran.
+
Buka Modul
+
+
+
+
+
+
+
Endpoint API Mobile (Preview)
+
Gunakan endpoint berikut untuk aplikasi Android/iOS hybrid:
+
+ /api/students.php — daftar siswa (GET)
+
-
+
+
+
diff --git a/student.php b/student.php
new file mode 100644
index 0000000..c369cfa
--- /dev/null
+++ b/student.php
@@ -0,0 +1,139 @@
+ 0) {
+ $stmt = $db->prepare("
+ SELECT s.*, c.name AS class_name, c.grade_level, c.homeroom_teacher
+ FROM students s
+ LEFT JOIN classes c ON c.id = s.class_id
+ WHERE s.id = :id
+ LIMIT 1
+ ");
+ $stmt->execute([':id' => $studentId]);
+ $student = $stmt->fetch();
+}
+
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
+$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+$projectName = $_SERVER['PROJECT_NAME'] ?? 'School Management System';
+?>
+
+
+
+
+
+
= htmlspecialchars($projectName) ?> — Detail Siswa
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Data siswa tidak ditemukan.
+
+
+
+
+
Profil Siswa
+
+
+
Kode Siswa
+
= htmlspecialchars($student['student_code']) ?>
+
+
+
Status
+
= htmlspecialchars(ucfirst($student['status'])) ?>
+
+
+
Nama Lengkap
+
= htmlspecialchars($student['full_name']) ?>
+
+
+
Jenis Kelamin
+
= htmlspecialchars($student['gender'] ?? '-') ?>
+
+
+
Kelas
+
= htmlspecialchars($student['class_name'] ?? '-') ?> (= htmlspecialchars($student['grade_level'] ?? '-') ?>)
+
+
+
Wali Kelas
+
= htmlspecialchars($student['homeroom_teacher'] ?? '-') ?>
+
+
+
Nama Wali
+
= htmlspecialchars($student['guardian_name'] ?? '-') ?>
+
+
+
Tanggal Input
+
= htmlspecialchars($student['created_at']) ?>
+
+
+
+
+
Catatan
+
= nl2br(htmlspecialchars($student['notes'])) ?>
+
+
+
+
+
+
Aksi Cepat
+
+
+
+
+
+
+ Modul lanjutan (absensi, ujian, keuangan) akan ditambahkan pada iterasi berikutnya.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/students.php b/students.php
new file mode 100644
index 0000000..bad33f2
--- /dev/null
+++ b/students.php
@@ -0,0 +1,236 @@
+prepare("INSERT INTO students (student_code, full_name, gender, class_id, guardian_name, status, notes)
+ VALUES (:code, :name, :gender, :class_id, :guardian, :status, :notes)");
+ $stmt->execute([
+ ':code' => $studentCode,
+ ':name' => $fullName,
+ ':gender' => $gender !== '' ? $gender : null,
+ ':class_id' => $classId,
+ ':guardian' => $guardianName !== '' ? $guardianName : null,
+ ':status' => $status,
+ ':notes' => $notes !== '' ? $notes : null,
+ ]);
+ header('Location: students.php?role=' . urlencode($role) . '&success=1');
+ exit;
+ } catch (PDOException $e) {
+ if ((int)$e->getCode() === 23000) {
+ $errors[] = 'Kode siswa sudah digunakan. Gunakan kode lain.';
+ } else {
+ $errors[] = 'Gagal menyimpan data siswa.';
+ }
+ }
+ }
+}
+
+$classes = $db->query("SELECT id, name, grade_level FROM classes ORDER BY grade_level, name")->fetchAll();
+$students = $db->query("
+ SELECT s.id, s.student_code, s.full_name, s.status, s.created_at,
+ c.name AS class_name, c.grade_level
+ FROM students s
+ LEFT JOIN classes c ON c.id = s.class_id
+ ORDER BY s.created_at DESC
+ LIMIT 50
+")->fetchAll();
+
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
+$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+$projectName = $_SERVER['PROJECT_NAME'] ?? 'School Management System';
+?>
+
+
+
+
+
+
= htmlspecialchars($projectName) ?> — Data Siswa
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Registrasi Siswa Baru
+
+
+
+
+
+
+ - = htmlspecialchars($err) ?>
+
+
+
+
+
+
+
+
+
+
Daftar Siswa Terbaru
+
+
+
+
Belum ada siswa yang terdaftar. Tambahkan data pertama di form.
+
+
+
+
+
+ | Kode |
+ Nama |
+ Kelas |
+ Status |
+ |
+
+
+
+
+
+
+ | = htmlspecialchars($student['student_code']) ?> |
+ = htmlspecialchars($student['full_name']) ?> |
+ = htmlspecialchars($student['class_name'] ?? '-') ?> |
+ = htmlspecialchars(ucfirst($student['status'])) ?> |
+
+ Detail
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Data siswa berhasil ditambahkan.
+
+
+
+
+
+
+