Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
463712fc0c |
33
api/students.php
Normal file
33
api/students.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?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),
|
||||
]);
|
||||
@ -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;
|
||||
.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;
|
||||
}
|
||||
@ -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';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
44
attendance.php
Normal file
44
attendance.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?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>
|
||||
71
attendance_detail.php
Normal file
71
attendance_detail.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?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>
|
||||
48
attendance_recap.php
Normal file
48
attendance_recap.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?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>
|
||||
22
db/migrations/001_school_core.sql
Normal file
22
db/migrations/001_school_core.sql
Normal file
@ -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;
|
||||
12
db/migrations/002_attendance_module.sql
Normal file
12
db/migrations/002_attendance_module.sql
Normal file
@ -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;
|
||||
51
includes/db_init.php
Normal file
51
includes/db_init.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?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
271
index.php
@ -1,150 +1,175 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
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';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<title><?= htmlspecialchars($projectName) ?> — Dashboard</title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<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;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>
|
||||
<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>
|
||||
<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 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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
|
||||
<footer class="footer text-center">
|
||||
© <?= date('Y') ?> EduCore CMS • Role aktif: <?= htmlspecialchars($roleLabels[$role]) ?>
|
||||
</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>
|
||||
|
||||
139
student.php
Normal file
139
student.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?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>
|
||||
236
students.php
Normal file
236
students.php
Normal file
@ -0,0 +1,236 @@
|
||||
<?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>
|
||||
Loading…
x
Reference in New Issue
Block a user