Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

12 changed files with 523 additions and 943 deletions

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/../includes/db_init.php';
$db = db();
ensure_schema($db);
seed_classes($db);
header('Content-Type: application/json; charset=utf-8');
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 50;
if ($limit <= 0 || $limit > 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),
]);

View File

@ -1,150 +1,403 @@
:root {
--bg: #f6f7f9;
--surface: #ffffff;
--border: #e3e6ea;
--text: #0f172a;
--muted: #6b7280;
--accent: #1d4ed8;
--accent-soft: rgba(29, 78, 216, 0.08);
}
* {
box-sizing: border-box;
}
body {
background: var(--bg);
color: var(--text);
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
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;
line-height: 1.5;
margin: 0;
min-height: 100vh;
}
a {
color: var(--accent);
.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 {
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;
}
a:hover {
color: #153eae;
/* 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;
}
.app-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.navbar {
border-bottom: 1px solid var(--border);
background: var(--surface);
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.navbar-brand {
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
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;
letter-spacing: 0.4px;
color: var(--muted);
border-bottom: 1px solid var(--border);
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
vertical-align: middle;
background: #fff;
padding: 1rem;
border: none;
}
.form-control,
.form-select {
border-radius: 8px;
border-color: var(--border);
font-size: 14px;
.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-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;
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
}
.btn-outline-secondary {
border-radius: 8px;
.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;
}
.section-title {
font-size: 16px;
.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;
margin-bottom: 12px;
width: 100%;
transition: all 0.3s ease;
}
.footer {
margin-top: auto;
padding: 24px 0;
color: var(--muted);
font-size: 12px;
.webhook-url {
font-size: 0.85em;
color: #555;
margin-top: 0.5rem;
}
.toast {
border-radius: 10px;
border: 1px solid var(--border);
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
.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);
}
.search-input {
max-width: 280px;
.history-table {
width: 100%;
}
.empty-state {
border: 1px dashed var(--border);
border-radius: 10px;
padding: 18px;
color: var(--muted);
background: #fafbfc;
.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;
}

View File

@ -1,31 +1,39 @@
document.addEventListener('DOMContentLoaded', () => {
const toastEl = document.getElementById('liveToast');
if (toastEl) {
const toast = new bootstrap.Toast(toastEl);
if (toastEl.dataset.autoshow === '1') {
toast.show();
}
}
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
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 appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
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';
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');
}
});
});

View File

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/db_init.php';
$db = db();
$role = $_GET['role'] ?? 'admin';
$action = $_GET['action'] ?? 'dashboard';
// Simple Teacher/Admin access control (just for demo)
$can_access = in_array($role, ['admin', 'guru']);
if (!$can_access) {
die("Akses ditolak.");
}
$classes = $db->query("SELECT * FROM classes ORDER BY name ASC")->fetchAll();
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Absensi Harian Dashboard Kelas</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>">
</head>
<body>
<div class="container py-4">
<nav class="mb-4"><a href="/?role=<?= htmlspecialchars($role) ?>"> Kembali ke Dashboard</a></nav>
<h1 class="h3 mb-4">Pilih Kelas untuk Absensi</h1>
<div class="row g-3">
<?php foreach ($classes as $class): ?>
<div class="col-md-4">
<div class="card p-3">
<h5 class="card-title"><?= htmlspecialchars($class['name']) ?></h5>
<p class="text-muted">Guru: <?= htmlspecialchars($class['homeroom_teacher'] ?? 'Belum ditentukan') ?></p>
<a href="attendance_detail.php?class_id=<?= (int)$class['id'] ?>&role=<?= htmlspecialchars($role) ?>" class="btn btn-primary btn-sm">Buka Absensi</a>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</body>
</html>

View File

@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/db_init.php';
$db = db();
$class_id = (int)$_GET['class_id'];
$date = $_GET['date'] ?? date('Y-m-d');
$role = $_GET['role'] ?? 'admin';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$attendance = $_POST['status'] ?? [];
foreach ($attendance as $student_id => $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();
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Absensi <?= htmlspecialchars($classData['name'] ?? 'Kelas') ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>">
</head>
<body>
<div class="container py-4">
<nav class="mb-4"><a href="attendance.php?role=<?= htmlspecialchars($role) ?>"> Kembali ke Daftar Kelas</a></nav>
<h1 class="h3 mb-4">Absensi Kelas: <?= htmlspecialchars($classData['name'] ?? 'Unknown') ?></h1>
<?php if (isset($message)): ?><div class="alert alert-success"><?= $message ?></div><?php endif; ?>
<form method="POST">
<table class="table table-bordered">
<thead>
<tr>
<th>Nama Siswa</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($studentList as $student): ?>
<tr>
<td><?= htmlspecialchars($student['full_name']) ?></td>
<td>
<select name="status[<?= (int)$student['id'] ?>]" class="form-select form-select-sm">
<option value="hadir" <?= ($student['status'] === 'hadir') ? 'selected' : '' ?>>Hadir</option>
<option value="sakit" <?= ($student['status'] === 'sakit') ? 'selected' : '' ?>>Sakit</option>
<option value="izin" <?= ($student['status'] === 'izin') ? 'selected' : '' ?>>Izin</option>
<option value="alpa" <?= ($student['status'] === 'alpa' || !$student['status']) ? 'selected' : '' ?>>Alpa</option>
</select>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<button type="submit" class="btn btn-primary">Simpan Absensi</button>
</form>
</div>
</body>
</html>

View File

@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/db_init.php';
$db = db();
$date = $_GET['date'] ?? date('Y-m-d');
$role = $_GET['role'] ?? 'admin';
$recap = $db->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();
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rekap Absensi <?= htmlspecialchars($date) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="p-4">
<nav class="mb-4"><a href="/?role=<?= htmlspecialchars($role) ?>"> Kembali ke Dashboard</a></nav>
<h1 class="h3 mb-4">Rekap Absensi: <?= htmlspecialchars($date) ?></h1>
<table class="table table-striped">
<thead>
<tr><th>Kelas</th><th>Hadir</th><th>Sakit</th><th>Izin</th><th>Alpa</th></tr>
</thead>
<tbody>
<?php foreach ($data as $row): ?>
<tr>
<td><?= htmlspecialchars($row['class_name']) ?></td>
<td><?= (int)$row['hadir'] ?></td>
<td><?= (int)$row['sakit'] ?></td>
<td><?= (int)$row['izin'] ?></td>
<td><?= (int)$row['alpa'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</body>
</html>

View File

@ -1,22 +0,0 @@
-- 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;

View File

@ -1,12 +0,0 @@
-- 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;

View File

@ -1,51 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
function ensure_schema(PDO $db): void {
$db->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],
]);
}
}

271
index.php
View File

@ -1,175 +1,150 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/db_init.php';
$db = db();
ensure_schema($db);
seed_classes($db);
$roles = ['admin', 'guru', 'orang_tua', 'siswa', 'bendahara'];
$role = $_GET['role'] ?? 'admin';
if (!in_array($role, $roles, true)) {
$role = 'admin';
}
$roleLabels = [
'admin' => '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';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="id">
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($projectName) ?> — Dashboard</title>
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- 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.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
: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>
<body>
<div class="app-shell">
<nav class="navbar navbar-expand-lg">
<div class="container-fluid px-4">
<a class="navbar-brand" href="/">EduCore CMS</a>
<div class="d-flex align-items-center gap-3 ms-auto">
<span class="badge badge-role">Role: <?= htmlspecialchars($roleLabels[$role]) ?></span>
<select id="roleSelect" class="form-select form-select-sm">
<?php foreach ($roles as $r): ?>
<option value="<?= htmlspecialchars($r) ?>" <?= $r === $role ? 'selected' : '' ?>><?= htmlspecialchars($roleLabels[$r]) ?></option>
<?php endforeach; ?>
</select>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
</div>
</nav>
<main class="container-fluid px-4 py-4">
<div class="hero mb-4">
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center justify-content-between gap-3">
<div>
<h1 class="h4 mb-2">Dashboard Operasional Sekolah</h1>
<p class="text-muted mb-0">Pantau data siswa, kelas, dan modul harian dari satu panel. Gunakan menu sesuai hak akses role.</p>
</div>
<div class="d-flex gap-2">
<a class="btn btn-primary" href="students.php?role=<?= urlencode($role) ?>">Kelola Siswa</a>
<a class="btn btn-outline-secondary" href="#api">Dokumentasi API</a>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card-soft p-3 h-100">
<div class="section-title">Ringkasan Data</div>
<div class="d-flex justify-content-between">
<div>
<div class="text-muted">Total Siswa</div>
<div class="h5 mb-2"><?= $studentCount ?></div>
</div>
<div>
<div class="text-muted">Total Kelas</div>
<div class="h5 mb-2"><?= $classCount ?></div>
</div>
</div>
<div class="text-muted small">
Terakhir ditambah: <?= $latestStudent ? htmlspecialchars($latestStudent['full_name']) . ' (' . htmlspecialchars($latestStudent['created_at']) . ')' : 'Belum ada data' ?>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card-soft p-3 h-100">
<div class="section-title">Menu Cepat (<?= htmlspecialchars($roleLabels[$role]) ?>)</div>
<div class="d-flex flex-wrap gap-2">
<?php foreach ($navItems[$role] as $item): ?>
<span class="badge text-bg-light border"><?= htmlspecialchars($item) ?></span>
<?php endforeach; ?>
</div>
<p class="text-muted small mt-3 mb-0">Menu ditampilkan sesuai RBAC. Modul yang belum tersedia akan segera ditambahkan.</p>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<div class="module-card card h-100">
<div class="card-body">
<div class="module-title">Master Data Siswa</div>
<div class="module-meta mb-3">Registrasi, kelas, wali, status</div>
<p class="text-muted small">Input data siswa baru, lihat daftar, dan detail profil.</p>
<a class="btn btn-sm btn-primary" href="students.php?role=<?= urlencode($role) ?>">Buka Modul</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="module-card card h-100">
<div class="card-body">
<div class="module-title">Absensi Harian</div>
<div class="module-meta mb-3">QR/manual + rekap</div>
<p class="text-muted small mb-3">Tandai hadir/sakit/izin dan lihat ringkasan kelas.</p>
<a class="btn btn-sm btn-primary" href="attendance.php?role=<?= urlencode($role) ?>">Buka Modul</a>
<a class="btn btn-sm btn-outline-primary" href="attendance_recap.php?role=<?= urlencode($role) ?>">Lihat Rekap</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="module-card card h-100">
<div class="card-body">
<div class="module-title">Keuangan & Tagihan</div>
<div class="module-meta mb-3">Pembayaran + verifikasi</div>
<p class="text-muted small mb-3">Kelola tagihan SPP dan konfirmasi pembayaran.</p>
<a class="btn btn-sm btn-primary" href="attendance.php?role=<?= urlencode($role) ?>">Buka Modul</a>
</div>
</div>
</div>
</div>
<div id="api" class="card-soft p-3 mt-4">
<div class="section-title">Endpoint API Mobile (Preview)</div>
<div class="small text-muted">Gunakan endpoint berikut untuk aplikasi Android/iOS hybrid:</div>
<ul class="mb-0">
<li><code>/api/students.php</code> daftar siswa (GET)</li>
</ul>
<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>
</main>
<footer class="footer text-center">
© <?= date('Y') ?> EduCore CMS • Role aktif: <?= htmlspecialchars($roleLabels[$role]) ?>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time(); ?>"></script>
</body>
</html>

View File

@ -1,139 +0,0 @@
<?php
declare(strict_types=1);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/db_init.php';
$db = db();
ensure_schema($db);
seed_classes($db);
$roles = ['admin', 'guru', 'orang_tua', 'siswa', 'bendahara'];
$role = $_GET['role'] ?? 'admin';
if (!in_array($role, $roles, true)) {
$role = 'admin';
}
$studentId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$student = null;
if ($studentId > 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';
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($projectName) ?> — Detail Siswa</title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>">
</head>
<body>
<div class="app-shell">
<nav class="navbar navbar-expand-lg">
<div class="container-fluid px-4">
<a class="navbar-brand" href="index.php?role=<?= urlencode($role) ?>">EduCore CMS</a>
<div class="ms-auto d-flex gap-2">
<a class="btn btn-outline-secondary btn-sm" href="students.php?role=<?= urlencode($role) ?>">Kembali ke Data Siswa</a>
</div>
</div>
</nav>
<main class="container-fluid px-4 py-4">
<?php if (!$student): ?>
<div class="alert alert-warning">Data siswa tidak ditemukan.</div>
<?php else: ?>
<div class="row g-3">
<div class="col-lg-7">
<div class="card-soft p-3 h-100">
<div class="section-title">Profil Siswa</div>
<div class="row g-3">
<div class="col-md-6">
<div class="text-muted small">Kode Siswa</div>
<div class="fw-semibold"><?= htmlspecialchars($student['student_code']) ?></div>
</div>
<div class="col-md-6">
<div class="text-muted small">Status</div>
<span class="badge text-bg-light border"><?= htmlspecialchars(ucfirst($student['status'])) ?></span>
</div>
<div class="col-md-6">
<div class="text-muted small">Nama Lengkap</div>
<div class="fw-semibold"><?= htmlspecialchars($student['full_name']) ?></div>
</div>
<div class="col-md-6">
<div class="text-muted small">Jenis Kelamin</div>
<div><?= htmlspecialchars($student['gender'] ?? '-') ?></div>
</div>
<div class="col-md-6">
<div class="text-muted small">Kelas</div>
<div><?= htmlspecialchars($student['class_name'] ?? '-') ?> (<?= htmlspecialchars($student['grade_level'] ?? '-') ?>)</div>
</div>
<div class="col-md-6">
<div class="text-muted small">Wali Kelas</div>
<div><?= htmlspecialchars($student['homeroom_teacher'] ?? '-') ?></div>
</div>
<div class="col-md-6">
<div class="text-muted small">Nama Wali</div>
<div><?= htmlspecialchars($student['guardian_name'] ?? '-') ?></div>
</div>
<div class="col-md-6">
<div class="text-muted small">Tanggal Input</div>
<div><?= htmlspecialchars($student['created_at']) ?></div>
</div>
</div>
<?php if (!empty($student['notes'])): ?>
<hr>
<div class="text-muted small mb-1">Catatan</div>
<div><?= nl2br(htmlspecialchars($student['notes'])) ?></div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-5">
<div class="card-soft p-3 h-100">
<div class="section-title">Aksi Cepat</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-secondary" disabled>Tambahkan Absensi</button>
<button class="btn btn-outline-secondary" disabled>Input Nilai/Ujian</button>
<button class="btn btn-outline-secondary" disabled>Tagihan Keuangan</button>
</div>
<div class="empty-state mt-3">
Modul lanjutan (absensi, ujian, keuangan) akan ditambahkan pada iterasi berikutnya.
</div>
</div>
</div>
</div>
<?php endif; ?>
</main>
<footer class="footer text-center">
© <?= date('Y') ?> EduCore CMS • Detail Siswa
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time(); ?>"></script>
</body>
</html>

View File

@ -1,236 +0,0 @@
<?php
declare(strict_types=1);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/db_init.php';
$db = db();
ensure_schema($db);
seed_classes($db);
$roles = ['admin', 'guru', 'orang_tua', 'siswa', 'bendahara'];
$role = $_GET['role'] ?? 'admin';
if (!in_array($role, $roles, true)) {
$role = 'admin';
}
$errors = [];
$success = isset($_GET['success']) ? (int)$_GET['success'] : 0;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$studentCode = trim($_POST['student_code'] ?? '');
$fullName = trim($_POST['full_name'] ?? '');
$gender = trim($_POST['gender'] ?? '');
$classId = (int)($_POST['class_id'] ?? 0);
$guardianName = trim($_POST['guardian_name'] ?? '');
$status = trim($_POST['status'] ?? 'aktif');
$notes = trim($_POST['notes'] ?? '');
if ($studentCode === '' || strlen($studentCode) < 3) {
$errors[] = 'Kode siswa wajib diisi (min 3 karakter).';
}
if ($fullName === '') {
$errors[] = 'Nama lengkap wajib diisi.';
}
if ($classId <= 0) {
$errors[] = 'Silakan pilih kelas.';
}
if ($status === '') {
$errors[] = 'Status siswa wajib dipilih.';
}
if (!$errors) {
try {
$stmt = $db->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';
?>
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($projectName) ?> — Data Siswa</title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>">
</head>
<body>
<div class="app-shell">
<nav class="navbar navbar-expand-lg">
<div class="container-fluid px-4">
<a class="navbar-brand" href="index.php?role=<?= urlencode($role) ?>">EduCore CMS</a>
<div class="ms-auto">
<a class="btn btn-outline-secondary btn-sm" href="index.php?role=<?= urlencode($role) ?>">Kembali ke Dashboard</a>
</div>
</div>
</nav>
<main class="container-fluid px-4 py-4">
<div class="row g-3">
<div class="col-lg-5">
<div class="card-soft p-3 h-100">
<div class="section-title">Registrasi Siswa Baru</div>
<form method="post" novalidate>
<div class="mb-2">
<label class="form-label">Kode Siswa</label>
<input type="text" name="student_code" class="form-control" placeholder="SIM-2026-001" required>
</div>
<div class="mb-2">
<label class="form-label">Nama Lengkap</label>
<input type="text" name="full_name" class="form-control" placeholder="Nama siswa" required>
</div>
<div class="mb-2">
<label class="form-label">Jenis Kelamin</label>
<select name="gender" class="form-select">
<option value="">Pilih</option>
<option value="Laki-laki">Laki-laki</option>
<option value="Perempuan">Perempuan</option>
</select>
</div>
<div class="mb-2">
<label class="form-label">Kelas</label>
<select name="class_id" class="form-select" required>
<option value="">Pilih kelas</option>
<?php foreach ($classes as $class): ?>
<option value="<?= (int)$class['id'] ?>"><?= htmlspecialchars($class['name']) ?> (Kelas <?= htmlspecialchars($class['grade_level']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-2">
<label class="form-label">Nama Wali</label>
<input type="text" name="guardian_name" class="form-control" placeholder="Nama orang tua/wali">
</div>
<div class="mb-2">
<label class="form-label">Status</label>
<select name="status" class="form-select" required>
<option value="aktif">Aktif</option>
<option value="mutasi">Mutasi</option>
<option value="lulus">Lulus</option>
<option value="alumni">Alumni</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Catatan</label>
<textarea name="notes" class="form-control" rows="3" placeholder="Catatan tambahan"></textarea>
</div>
<button class="btn btn-primary w-100" type="submit">Simpan Data Siswa</button>
</form>
<?php if ($errors): ?>
<div class="alert alert-danger mt-3 mb-0">
<ul class="mb-0">
<?php foreach ($errors as $err): ?>
<li><?= htmlspecialchars($err) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-7">
<div class="card-soft p-3 h-100">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
<div class="section-title mb-0">Daftar Siswa Terbaru</div>
<input id="studentSearch" type="text" class="form-control form-control-sm search-input" placeholder="Cari nama atau kode">
</div>
<?php if (!$students): ?>
<div class="empty-state mt-3">Belum ada siswa yang terdaftar. Tambahkan data pertama di form.</div>
<?php else: ?>
<div class="table-responsive mt-3">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Kode</th>
<th>Nama</th>
<th>Kelas</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<?php
$searchHay = strtolower($student['student_code'] . ' ' . $student['full_name'] . ' ' . ($student['class_name'] ?? ''));
?>
<tr data-student-row="<?= htmlspecialchars($searchHay) ?>">
<td><?= htmlspecialchars($student['student_code']) ?></td>
<td><?= htmlspecialchars($student['full_name']) ?></td>
<td><?= htmlspecialchars($student['class_name'] ?? '-') ?></td>
<td><span class="badge text-bg-light border"><?= htmlspecialchars(ucfirst($student['status'])) ?></span></td>
<td class="text-end">
<a class="btn btn-sm btn-outline-secondary" href="student.php?id=<?= (int)$student['id'] ?>&role=<?= urlencode($role) ?>">Detail</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</main>
<footer class="footer text-center">
© <?= date('Y') ?> EduCore CMS • Modul Data Siswa
</footer>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autoshow="<?= $success ? '1' : '0' ?>">
<div class="toast-header">
<strong class="me-auto">Sukses</strong>
<small>Baru saja</small>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">Data siswa berhasil ditambahkan.</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time(); ?>"></script>
</body>
</html>