1041 lines
28 KiB
HTML
1041 lines
28 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="id">
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Pengaturan — Bel Sekolah</title>
|
||
<style>
|
||
/* Reset & Base */
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||
background-color: #f1f5f9;
|
||
color: #334155;
|
||
}
|
||
|
||
/* Layout Utilities */
|
||
.flex {
|
||
display: flex;
|
||
}
|
||
|
||
.flex-col {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.items-center {
|
||
align-items: center;
|
||
}
|
||
|
||
.justify-between {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.justify-center {
|
||
justify-content: center;
|
||
}
|
||
|
||
.justify-end {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.h-screen {
|
||
height: 100vh;
|
||
}
|
||
|
||
.overflow-hidden {
|
||
overflow: hidden;
|
||
}
|
||
|
||
.overflow-y-auto {
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.overflow-x-auto {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.flex-1 {
|
||
flex: 1;
|
||
}
|
||
|
||
.w-64 {
|
||
width: 16rem;
|
||
}
|
||
|
||
.w-full {
|
||
width: 100%;
|
||
}
|
||
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
.block {
|
||
display: block;
|
||
}
|
||
|
||
.absolute {
|
||
position: absolute;
|
||
}
|
||
|
||
.relative {
|
||
position: relative;
|
||
}
|
||
|
||
.fixed {
|
||
position: fixed;
|
||
}
|
||
|
||
.inset-0 {
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
}
|
||
|
||
.z-50 {
|
||
z-index: 50;
|
||
}
|
||
|
||
.z-40 {
|
||
z-index: 40;
|
||
}
|
||
|
||
.z-10 {
|
||
z-index: 10;
|
||
}
|
||
|
||
/* Spacing & Sizing */
|
||
.p-4 {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.p-6 {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.px-4 {
|
||
padding-left: 1rem;
|
||
padding-right: 1rem;
|
||
}
|
||
|
||
.py-2 {
|
||
padding-top: 0.5rem;
|
||
padding-bottom: 0.5rem;
|
||
}
|
||
|
||
.py-3 {
|
||
padding-top: 0.75rem;
|
||
padding-bottom: 0.75rem;
|
||
}
|
||
|
||
.mb-2 {
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.mb-4 {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.mb-6 {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.mt-4 {
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.mr-2 {
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.gap-2 {
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.gap-4 {
|
||
gap: 1rem;
|
||
}
|
||
|
||
/* Hide Spinners */
|
||
input[type=number] {
|
||
text-align: center;
|
||
}
|
||
|
||
/* Force min-width on inputs for mobile */
|
||
@media (max-width: 768px) {
|
||
|
||
/* Hide Spinners on Mobile only */
|
||
input[type=number]::-webkit-inner-spin-button,
|
||
input[type=number]::-webkit-outer-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
|
||
input[type=number] {
|
||
-moz-appearance: textfield;
|
||
min-width: 35px;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
font-size: 0.75rem;
|
||
padding: 0.2rem;
|
||
}
|
||
}
|
||
|
||
/* Typography */
|
||
.font-bold {
|
||
font-weight: 700;
|
||
}
|
||
|
||
.font-semibold {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.font-medium {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.text-sm {
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.text-xs {
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.text-xl {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.text-2xl {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.text-center {
|
||
text-align: center;
|
||
}
|
||
|
||
.text-white {
|
||
color: white;
|
||
}
|
||
|
||
.text-blue-600 {
|
||
color: #2563eb;
|
||
}
|
||
|
||
.text-green-600 {
|
||
color: #16a34a;
|
||
}
|
||
|
||
.text-red-500 {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.text-slate-300 {
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
.text-slate-400 {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.text-slate-500 {
|
||
color: #64748b;
|
||
}
|
||
|
||
.text-slate-600 {
|
||
color: #475569;
|
||
}
|
||
|
||
.text-slate-700 {
|
||
color: #334155;
|
||
}
|
||
|
||
.text-slate-800 {
|
||
color: #1e293b;
|
||
}
|
||
|
||
|
||
/* Components */
|
||
.bg-white {
|
||
background-color: white;
|
||
}
|
||
|
||
.bg-slate-800 {
|
||
background-color: #1e293b;
|
||
}
|
||
|
||
.bg-slate-900 {
|
||
background-color: #0f172a;
|
||
}
|
||
|
||
.bg-slate-100 {
|
||
background-color: #f1f5f9;
|
||
}
|
||
|
||
.bg-slate-50 {
|
||
background-color: #f8fafc;
|
||
}
|
||
|
||
.shadow {
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.rounded {
|
||
border-radius: 0.375rem;
|
||
}
|
||
|
||
.rounded-xl {
|
||
border-radius: 0.75rem;
|
||
}
|
||
|
||
.border {
|
||
border: 1px solid #cbd5e1;
|
||
}
|
||
|
||
.border-b {
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.border-t {
|
||
border-top: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.cursor-pointer {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.hover\:bg-slate-50:hover {
|
||
background-color: #f8fafc;
|
||
}
|
||
|
||
|
||
/* Sidebar */
|
||
.sidebar {
|
||
transition: transform 0.3s ease-in-out;
|
||
}
|
||
|
||
.sidebar-link {
|
||
display: block;
|
||
padding: 0.75rem 1rem;
|
||
color: #cbd5e1;
|
||
text-decoration: none;
|
||
border-radius: 0.375rem;
|
||
margin-bottom: 0.25rem;
|
||
transition: background-color 0.2s, color 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.sidebar-link:hover {
|
||
background-color: #334155;
|
||
color: white;
|
||
}
|
||
|
||
.sidebar-link.active {
|
||
background-color: #2563eb;
|
||
color: white;
|
||
}
|
||
|
||
/* Buttons */
|
||
input[type="text"],
|
||
input[type="password"],
|
||
input[type="number"] {
|
||
width: 100%;
|
||
border: 1px solid #cbd5e1;
|
||
padding: 0.5rem;
|
||
border-radius: 0.375rem;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 0.5rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border: none;
|
||
color: white;
|
||
text-decoration: none;
|
||
text-align: center;
|
||
}
|
||
|
||
.btn.small {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.btn-blue {
|
||
background-color: #2563eb;
|
||
}
|
||
|
||
.btn-blue:hover {
|
||
background-color: #1d4ed8;
|
||
}
|
||
|
||
.btn-green {
|
||
background-color: #16a34a;
|
||
}
|
||
|
||
.btn-green:hover {
|
||
background-color: #15803d;
|
||
}
|
||
|
||
.btn-red {
|
||
background-color: #dc2626;
|
||
}
|
||
|
||
.btn-red:hover {
|
||
background-color: #b91c1c;
|
||
}
|
||
|
||
.btn-gray {
|
||
background-color: #64748b;
|
||
}
|
||
|
||
.btn-gray:hover {
|
||
background-color: #475569;
|
||
}
|
||
|
||
.btn-purple {
|
||
background-color: #7c3aed;
|
||
}
|
||
|
||
.btn-purple:hover {
|
||
background-color: #6d28d9;
|
||
}
|
||
|
||
/* Table */
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th {
|
||
text-align: left;
|
||
background-color: #f1f5f9;
|
||
padding: 0.5rem;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
td {
|
||
padding: 0.5rem;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
}
|
||
|
||
/* Mobile Responsive */
|
||
.mobile-header {
|
||
display: none;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.layout-container {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
z-index: 50;
|
||
width: 16rem;
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.sidebar.open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.content-area {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.mobile-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1rem;
|
||
background-color: #1e293b;
|
||
color: white;
|
||
}
|
||
|
||
.overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 40;
|
||
}
|
||
|
||
.overlay.open {
|
||
display: block;
|
||
}
|
||
|
||
/* Table adjust */
|
||
/* Force min-width on inputs for mobile */
|
||
input[type="number"] {
|
||
min-width: 35px;
|
||
padding: 0.1rem;
|
||
}
|
||
|
||
th,
|
||
td {
|
||
font-size: 0.75rem;
|
||
padding: 0.1rem;
|
||
}
|
||
|
||
.th-desc {
|
||
min-width: 50px;
|
||
}
|
||
|
||
.col-index {
|
||
display: none;
|
||
}
|
||
|
||
.btn.small {
|
||
padding: 0.15rem 0.3rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
}
|
||
|
||
/* Desktop default */
|
||
.th-desc {
|
||
min-width: 150px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<!-- Login Overlay -->
|
||
<div id="loginOverlay" class="h-screen w-full flex items-center justify-center bg-slate-50 absolute z-50">
|
||
<div style="display: flex; align-items: center; justify-content: center; height: 100%; width: 100%;">
|
||
<div class="bg-white p-6 rounded-xl shadow w-full max-w-sm mx-4">
|
||
<h1 class="text-2xl font-bold text-center text-blue-600 mb-6">🔐 Login Admin</h1>
|
||
<form onsubmit="login(event)">
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-1 text-slate-700">Username</label>
|
||
<input id="loginUser" type="text" placeholder="admin" required>
|
||
</div>
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium mb-1 text-slate-700">Password</label>
|
||
<input id="loginPass" type="password" placeholder="******" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-blue w-full">Masuk Dashboard</button>
|
||
</form>
|
||
<div id="loginError" class="mt-4 text-center text-red-500 text-sm hidden"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dashboard Layout -->
|
||
<div id="dashboard" class="flex h-screen overflow-hidden layout-container hidden">
|
||
|
||
<!-- Mobile Header -->
|
||
<div class="mobile-header">
|
||
<div class="flex items-center gap-3">
|
||
<button onclick="toggleSidebar()" class="text-white text-2xl focus:outline-none">☰</button>
|
||
<span class="font-bold text-lg" id="mobileTitle">Bel Sekolah</span>
|
||
</div>
|
||
<div class="text-xs bg-green-600 px-2 py-1 rounded text-white hidden" id="statusBadge">Online</div>
|
||
</div>
|
||
|
||
<!-- Backdrop -->
|
||
<div id="sidebarOverlay" class="overlay" onclick="toggleSidebar()"></div>
|
||
|
||
<!-- Sidebar -->
|
||
<aside id="sidebar" class="sidebar w-64 bg-slate-800 text-slate-300 flex flex-col">
|
||
<div class="p-4 border-b border-slate-700 bg-slate-900 flex justify-between items-center">
|
||
<h1 class="text-xl font-bold text-white flex items-center gap-2">
|
||
🔔 <span id="sidebarTitle">Bel Sekolah</span>
|
||
</h1>
|
||
<!-- Close button for mobile -->
|
||
<button onclick="toggleSidebar()" class="text-slate-400 md:hidden text-xl">✕</button>
|
||
</div>
|
||
<nav class="flex-1 overflow-y-auto p-2">
|
||
<a onclick="switchTab('tab-jadwal')" class="sidebar-link active" id="link-tab-jadwal">📅 Jadwal Pelajaran</a>
|
||
<a onclick="switchTab('tab-config')" class="sidebar-link" id="link-tab-config">⚙️ Konfigurasi Umum</a>
|
||
<a onclick="switchTab('tab-backup')" class="sidebar-link" id="link-tab-backup">💾 Data & Backup</a>
|
||
<a onclick="switchTab('tab-admin')" class="sidebar-link" id="link-tab-admin">🔐 Akun Admin</a>
|
||
</nav>
|
||
<div class="p-4 border-t border-slate-700">
|
||
<div class="text-xs text-slate-500 mb-2">System Status</div>
|
||
<button onclick="stopBel()" class="btn btn-red w-full text-sm mb-2">⛔ Stop Bel</button>
|
||
<a href="/" class="btn btn-gray w-full text-sm text-center block">🏠 Home Page</a>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main Content -->
|
||
<main class="flex-1 overflow-y-auto bg-slate-50 p-6 content-area">
|
||
<div id="content-container" class="max-w-4xl mx-auto">
|
||
|
||
<!-- Tab: Jadwal -->
|
||
<div id="tab-jadwal" class="tab-content block">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-2xl font-bold text-slate-800">Jadwal</h2>
|
||
<div class="flex gap-2">
|
||
<button onclick="tambah()" class="btn btn-green small">➕ Tambah</button>
|
||
<button onclick="simpan()" class="btn btn-blue small">💾 Simpan</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white rounded-xl shadow overflow-hidden">
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full">
|
||
<thead>
|
||
<tr>
|
||
<th class="w-10 text-center col-index">#</th>
|
||
<th class="w-12 text-center">On</th>
|
||
<th class="w-10">Jam</th>
|
||
<th class="w-10">Mnt</th>
|
||
<th class="w-12">Trk</th>
|
||
<th class="th-desc">Deskripsi</th>
|
||
<th class="w-20 text-center">Aksi</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="body"></tbody>
|
||
</table>
|
||
</div>
|
||
<div id="emptyJadwal" class="hidden p-8 text-center text-slate-400">
|
||
Belum ada jadwal.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Konfigurasi -->
|
||
<div id="tab-config" class="tab-content hidden">
|
||
<h2 class="text-2xl font-bold text-slate-800 mb-6">Konfigurasi</h2>
|
||
|
||
<div class="grid gap-6">
|
||
<div class="bg-white p-6 rounded-xl shadow">
|
||
<h3 class="font-semibold text-lg mb-4">🏫 Identitas Sekolah</h3>
|
||
<div class="flex gap-2">
|
||
<input id="schoolName" type="text" placeholder="Nama Sekolah..." class="flex-1">
|
||
<button onclick="saveName()" class="btn btn-blue">Simpan</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white p-6 rounded-xl shadow">
|
||
<h3 class="font-semibold text-lg mb-4">📅 Hari Libur</h3>
|
||
<div class="flex items-center gap-3 bg-slate-50 p-3 rounded border">
|
||
<input type="checkbox" id="skipSunday" style="width: 1.25rem; height: 1.25rem;">
|
||
<label for="skipSunday" class="font-medium cursor-pointer select-none text-slate-700">Libur hari Minggu
|
||
(Bel mati)</label>
|
||
</div>
|
||
<div class="mt-3 text-right">
|
||
<button onclick="saveSkipSunday()" class="btn btn-blue">Simpan</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white p-6 rounded-xl shadow">
|
||
<h3 class="font-semibold text-lg mb-4">🕒 Waktu Sistem</h3>
|
||
<button onclick="syncTime()" class="btn btn-purple">🕒 Sync Waktu Browser</button>
|
||
</div>
|
||
|
||
<div class="bg-white p-6 rounded-xl shadow">
|
||
<h3 class="font-semibold text-lg mb-4">📡 Update Firmware</h3>
|
||
<div id="otaInfo" class="text-sm text-slate-600">
|
||
Klik cek info untuk detail OTA.
|
||
</div>
|
||
<button onclick="loadOTAInfo()" class="btn btn-gray small mt-2">Cek Info OTA</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Backup -->
|
||
<div id="tab-backup" class="tab-content hidden">
|
||
<h2 class="text-2xl font-bold text-slate-800 mb-6">Backup Data</h2>
|
||
|
||
<div class="bg-white p-6 rounded-xl shadow mb-6">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-semibold text-lg">💾 Backup Internal</h3>
|
||
<button onclick="saveInternalBackup()" class="btn btn-blue small">➕ Baru</button>
|
||
</div>
|
||
|
||
<div class="border rounded bg-slate-50">
|
||
<div id="backupList" class="divide-y max-h-64 overflow-y-auto">
|
||
<p class="p-4 text-center text-slate-400 text-sm">Memuat list...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white p-6 rounded-xl shadow">
|
||
<h3 class="font-semibold text-lg mb-4">💻 Ekspor / Impor JSON</h3>
|
||
<div class="flex gap-3">
|
||
<div class="flex-1 border rounded p-3 text-center hover:bg-slate-50 cursor-pointer" onclick="backup()">
|
||
<div class="text-2xl mb-1">⬇️</div>
|
||
<div class="font-bold text-blue-600 text-sm">Download</div>
|
||
</div>
|
||
<div class="flex-1 border rounded p-3 text-center hover:bg-slate-50 cursor-pointer"
|
||
onclick="document.getElementById('restoreFile').click()">
|
||
<div class="text-2xl mb-1">⬆️</div>
|
||
<div class="font-bold text-green-600 text-sm">Upload</div>
|
||
<input type="file" id="restoreFile" accept=".json" class="hidden" onchange="restoreFileSelected(this)">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: Admin -->
|
||
<div id="tab-admin" class="tab-content hidden">
|
||
<h2 class="text-2xl font-bold text-slate-800 mb-6">Ganti Password</h2>
|
||
<div class="bg-white p-6 rounded-xl shadow max-w-lg">
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-1 text-slate-700">Username Baru</label>
|
||
<input id="userInput" type="text">
|
||
</div>
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium mb-1 text-slate-700">Password Baru</label>
|
||
<input id="passInput" type="password">
|
||
</div>
|
||
<button onclick="changeAdmin()" class="btn btn-purple w-full">💾 Simpan Perubahan</button>
|
||
|
||
<div class="mt-8 pt-4 border-t text-center">
|
||
<a href="https://wa.me/628113936644?text=Perlu%20Bantuan%3F" target="_blank"
|
||
class="text-green-600 font-medium hover:underline text-sm">
|
||
💬 Hubungi Bantuan (WhatsApp)
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
// --- UI Logic ---
|
||
function toggleSidebar() {
|
||
const sidebar = document.getElementById('sidebar');
|
||
const overlay = document.getElementById('sidebarOverlay');
|
||
sidebar.classList.toggle('open');
|
||
overlay.classList.toggle('open');
|
||
}
|
||
|
||
// Auto close sidebar on mobile when link clicked
|
||
function switchTab(tabId) {
|
||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('block'));
|
||
|
||
document.getElementById(tabId).classList.remove('hidden');
|
||
document.getElementById(tabId).classList.add('block');
|
||
|
||
document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active'));
|
||
document.getElementById('link-' + tabId).classList.add('active');
|
||
|
||
if (window.innerWidth <= 768) {
|
||
toggleSidebar(); // Close menu
|
||
}
|
||
|
||
if (tabId === 'tab-backup') loadBackupList();
|
||
}
|
||
|
||
// --- State & Auth ---
|
||
let jadwal = [];
|
||
let isLoggedIn = false;
|
||
|
||
async function login(event) {
|
||
event.preventDefault();
|
||
const user = document.getElementById('loginUser').value.trim();
|
||
const pass = document.getElementById('loginPass').value.trim();
|
||
|
||
if (!user || !pass) return showLoginError('Masukkan username & password');
|
||
|
||
try {
|
||
const res = await fetch('/api/validate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ user, pass })
|
||
});
|
||
const result = await res.json();
|
||
|
||
if (result.valid) {
|
||
isLoggedIn = true;
|
||
document.getElementById('loginOverlay').classList.add('hidden');
|
||
document.getElementById('dashboard').classList.remove('hidden');
|
||
loadJadwal();
|
||
loadConfig();
|
||
} else {
|
||
showLoginError('Username atau password salah!');
|
||
}
|
||
} catch (e) { showLoginError('Gagal terhubung ke perangkat. Coba refresh.'); }
|
||
}
|
||
|
||
function showLoginError(msg) {
|
||
const el = document.getElementById('loginError');
|
||
el.textContent = msg;
|
||
el.classList.remove('hidden');
|
||
}
|
||
|
||
// --- Data Rendering ---
|
||
async function loadJadwal() {
|
||
if (!isLoggedIn) return;
|
||
const r = await fetch('/api/jadwal');
|
||
const d = await r.json();
|
||
jadwal = d || [];
|
||
render();
|
||
}
|
||
|
||
function render() {
|
||
const body = document.getElementById('body');
|
||
const empty = document.getElementById('emptyJadwal');
|
||
body.innerHTML = '';
|
||
|
||
if (!jadwal.length) {
|
||
empty.classList.remove('hidden');
|
||
return;
|
||
}
|
||
empty.classList.add('hidden');
|
||
|
||
jadwal.forEach((j, i) => {
|
||
const tr = document.createElement('tr');
|
||
tr.className = 'hover:bg-slate-50';
|
||
tr.innerHTML = `
|
||
<td class="text-center text-slate-500 col-index">${i + 1}</td>
|
||
<td class="text-center"><input type="checkbox" ${j.enabled !== false ? 'checked' : ''} style="width:1.2rem; height:1.2rem;"></td>
|
||
<td><input type="number" min="0" max="23" value="${j.jam}"></td>
|
||
<td><input type="number" min="0" max="59" value="${j.menit}"></td>
|
||
<td><input type="number" min="1" value="${j.track || j.trackStart || 1}"></td>
|
||
<td><input type="text" value="${j.desc || ''}" placeholder="Ket..."></td>
|
||
<td class="text-center">
|
||
<div class="flex justify-center gap-1">
|
||
<button onclick="preview(${j.track || j.trackStart || 1})" class="btn btn-green small">▶</button>
|
||
<button onclick="del(${i})" class="btn btn-red small">✕</button>
|
||
</div>
|
||
</td>`;
|
||
body.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function tambah() {
|
||
if (!isLoggedIn) return;
|
||
if (jadwal.length >= 20) return alert('Maksimal 20 jadwal');
|
||
jadwal.push({ jam: 7, menit: 0, track: 1, desc: 'Bel Masuk', enabled: true });
|
||
render();
|
||
}
|
||
|
||
function del(i) {
|
||
if (!isLoggedIn) return;
|
||
if (confirm('Hapus jadwal ini?')) {
|
||
jadwal.splice(i, 1);
|
||
render();
|
||
}
|
||
}
|
||
|
||
async function simpan() {
|
||
if (!isLoggedIn) return;
|
||
const rows = document.querySelectorAll('#body tr');
|
||
const arr = [];
|
||
rows.forEach(r => {
|
||
const inputs = r.querySelectorAll('input');
|
||
if (inputs.length >= 5) {
|
||
arr.push({
|
||
jam: parseInt(inputs[1].value || 0),
|
||
menit: parseInt(inputs[2].value || 0),
|
||
track: parseInt(inputs[3].value || 1),
|
||
desc: inputs[4].value || '',
|
||
enabled: inputs[0].checked
|
||
});
|
||
}
|
||
});
|
||
|
||
await fetch('/api/save', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(arr)
|
||
});
|
||
alert('✅ Jadwal Tersimpan!');
|
||
loadJadwal();
|
||
}
|
||
|
||
// --- API Calls ---
|
||
async function loadConfig() {
|
||
if (!isLoggedIn) return;
|
||
const r = await fetch('/api/config');
|
||
const d = await r.json();
|
||
if (d.schoolName) {
|
||
document.getElementById('schoolName').value = d.schoolName;
|
||
document.getElementById('sidebarTitle').textContent = d.schoolName;
|
||
document.getElementById('mobileTitle').textContent = d.schoolName;
|
||
}
|
||
document.getElementById('skipSunday').checked = d.skipSunday || false;
|
||
if (d.user) document.getElementById('userInput').placeholder = d.user;
|
||
}
|
||
|
||
async function saveName() {
|
||
const name = document.getElementById('schoolName').value.trim();
|
||
await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ schoolName: name })
|
||
});
|
||
document.getElementById('sidebarTitle').textContent = name;
|
||
document.getElementById('mobileTitle').textContent = name;
|
||
alert('Nama Sekolah Disimpan');
|
||
}
|
||
|
||
async function saveSkipSunday() {
|
||
const skip = document.getElementById('skipSunday').checked;
|
||
await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ skipSunday: skip })
|
||
});
|
||
alert('Pengaturan Disimpan');
|
||
}
|
||
|
||
async function syncTime() {
|
||
const now = new Date();
|
||
const epoch = Math.floor((now.getTime() - now.getTimezoneOffset() * 60000) / 1000);
|
||
await fetch('/api/settime', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ epoch })
|
||
});
|
||
alert('✅ Waktu tersinkronisasi!');
|
||
}
|
||
|
||
async function changeAdmin() {
|
||
const u = document.getElementById('userInput').value.trim();
|
||
const p = document.getElementById('passInput').value.trim();
|
||
if (!u || !p) return alert('Isi user & pass baru');
|
||
|
||
const res = await fetch('/api/admin', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ user: u, pass: p })
|
||
});
|
||
if (res.ok) {
|
||
alert('Login diganti. Login ulang.');
|
||
location.reload();
|
||
} else alert('Gagal');
|
||
}
|
||
|
||
async function preview(track) { await fetch(`/api/play?track=${track}`); }
|
||
async function stopBel() { await fetch('/api/stop'); }
|
||
|
||
async function loadOTAInfo() {
|
||
try {
|
||
const r = await fetch('/api/ota');
|
||
const d = await r.json();
|
||
document.getElementById('otaInfo').innerHTML = `Host: <b class="font-mono">${d.hostname}</b><br>Pass: <b class="font-mono">${d.password}</b>`;
|
||
} catch (e) { alert('Gagal load info'); }
|
||
}
|
||
|
||
// --- Backup Logic ---
|
||
async function loadBackupList() {
|
||
const listDiv = document.getElementById('backupList');
|
||
try {
|
||
const res = await fetch('/api/backup/list');
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
const text = await res.text();
|
||
let files;
|
||
try { files = JSON.parse(text); } catch (e) { files = []; }
|
||
|
||
if (!Array.isArray(files) || files.length === 0) {
|
||
listDiv.innerHTML = '<div class="p-4 text-center text-slate-400 text-sm">Kosong.</div>';
|
||
return;
|
||
}
|
||
|
||
files.sort((a, b) => b.name.localeCompare(a.name));
|
||
let html = '';
|
||
files.forEach(f => {
|
||
const nameDisplay = f.name.replace('/backup_', '').replace('.json', '').replace('_', ' ');
|
||
html += `
|
||
<div class="flex items-center justify-between p-3 hover:bg-slate-50 group">
|
||
<div class="flex items-center gap-3 overflow-hidden">
|
||
<div class="text-lg">📄</div>
|
||
<div class="min-w-0">
|
||
<div class="font-medium text-sm text-slate-700 truncate">${nameDisplay}</div>
|
||
<div class="text-xs text-slate-400">${f.size || 0} b</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button onclick="loadInternal('${f.name}')" class="btn btn-blue small" title="Restore">♻️</button>
|
||
<button onclick="delInternal('${f.name}')" class="btn btn-red small" title="Hapus">✕</button>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
listDiv.innerHTML = html;
|
||
} catch (e) {
|
||
listDiv.innerHTML = `<div class="p-4 text-center text-red-500 text-sm">Error: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
async function saveInternalBackup() {
|
||
if (!confirm('Backup sekarang?')) return;
|
||
try {
|
||
const res = await fetch('/api/backup/save', { method: 'POST' });
|
||
if (res.ok) { alert('✅ Backup OK'); loadBackupList(); }
|
||
else throw new Error('Failed');
|
||
} catch (e) { alert('Gagal backup'); }
|
||
}
|
||
|
||
async function loadInternal(fname) {
|
||
if (!confirm(`Restore dari ${fname}?`)) return;
|
||
try {
|
||
const res = await fetch('/api/backup/load', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ filename: fname })
|
||
});
|
||
if (res.ok) { alert('✅ Restore OK. Reloading...'); location.reload(); }
|
||
} catch (e) { alert('Gagal restore'); }
|
||
}
|
||
|
||
async function delInternal(fname) {
|
||
if (!confirm(`Hapus ${fname}?`)) return;
|
||
try {
|
||
const res = await fetch('/api/backup/delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ filename: fname })
|
||
});
|
||
if (res.ok) loadBackupList();
|
||
} catch (e) { alert('Gagal hapus'); }
|
||
}
|
||
|
||
async function backup() {
|
||
try {
|
||
const res = await fetch('/api/backup/download');
|
||
const data = await res.json();
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url; a.download = 'backup.json';
|
||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||
} catch (e) { alert('Gagal download'); }
|
||
}
|
||
|
||
async function restoreFileSelected(input) {
|
||
const file = input.files[0];
|
||
if (!file) return;
|
||
try {
|
||
const text = await file.text();
|
||
const data = JSON.parse(text);
|
||
const res = await fetch('/api/restore', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (res.ok) { alert('Restore OK!'); location.reload(); }
|
||
else throw new Error('API Error');
|
||
} catch (e) { alert('Gagal upload: ' + e.message); }
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |