Files
bel-sekolah-esp32/data/setting.html

1242 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;
}
/* Day tabs */
.day-tab {
background-color: transparent;
color: #64748b;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.day-tab:hover {
background-color: #e2e8f0;
}
.day-tab.active {
background-color: #2563eb;
color: white;
}
.day-tab[data-day="0"],
.day-tab[data-day="6"] {
color: #dc2626;
}
.day-tab[data-day="0"].active,
.day-tab[data-day="6"].active {
background-color: #dc2626;
color: white;
}
</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-4">
<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>
<!-- Day Tabs (only shown in 7-day mode) -->
<div id="dayTabsContainer" class="hidden mb-4">
<div class="flex flex-wrap gap-1 bg-slate-100 p-1 rounded-lg">
<button onclick="switchDayTab(0)" class="day-tab px-3 py-2 rounded text-sm font-medium"
data-day="0">Minggu</button>
<button onclick="switchDayTab(1)" class="day-tab px-3 py-2 rounded text-sm font-medium active"
data-day="1">Senin</button>
<button onclick="switchDayTab(2)" class="day-tab px-3 py-2 rounded text-sm font-medium"
data-day="2">Selasa</button>
<button onclick="switchDayTab(3)" class="day-tab px-3 py-2 rounded text-sm font-medium"
data-day="3">Rabu</button>
<button onclick="switchDayTab(4)" class="day-tab px-3 py-2 rounded text-sm font-medium"
data-day="4">Kamis</button>
<button onclick="switchDayTab(5)" class="day-tab px-3 py-2 rounded text-sm font-medium"
data-day="5">Jumat</button>
<button onclick="switchDayTab(6)" class="day-tab px-3 py-2 rounded text-sm font-medium"
data-day="6">Sabtu</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 untuk hari ini.
</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">📅 Mode Jadwal</h3>
<div class="mb-4">
<label class="block text-sm font-medium mb-2 text-slate-700">Pilih Mode:</label>
<select id="scheduleMode" class="w-full border rounded p-2" onchange="onScheduleModeChange()">
<option value="0">1 Hari (Sama Setiap Hari)</option>
<option value="1">7 Hari (Berbeda Tiap Hari)</option>
</select>
</div>
<div id="skipSundayContainer" class="flex items-center gap-3 bg-slate-50 p-3 rounded border mb-3">
<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>
<p class="text-xs text-slate-500 mb-3" id="scheduleModeHint">Mode 1 Hari: Jadwal sama setiap hari. Centang
opsi di atas untuk menonaktifkan bel di hari Minggu.</p>
<div class="text-right">
<button onclick="saveScheduleMode()" class="btn btn-blue">Simpan Mode</button>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow">
<h3 class="font-semibold text-lg mb-4">🔔 Track Bel Manual</h3>
<p class="text-sm text-slate-500 mb-3">Track MP3 yang diputar saat tombol TEST ditekan lama (2 detik)</p>
<div class="flex gap-2">
<input id="testTrack" type="number" min="1" max="9999" placeholder="1000" style="width: 100px;">
<button onclick="saveTestTrack()" class="btn btn-blue">Simpan</button>
<button onclick="previewTestTrack()" class="btn btn-green">▶ Test</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;
let currentScheduleMode = 0; // 0 = 1-hari, 1 = 7-hari
let currentDayTab = 1; // Default to Senin (1)
const dayNames = ['M', 'S', 'S', 'R', 'K', 'J', 'S']; // Minggu, Senin, Selasa, Rabu, Kamis, Jumat, Sabtu
const dayLabels = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
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');
const dayTabsContainer = document.getElementById('dayTabsContainer');
body.innerHTML = '';
// Show/hide day tabs based on mode
if (currentScheduleMode === 1) {
dayTabsContainer.classList.remove('hidden');
} else {
dayTabsContainer.classList.add('hidden');
}
// Filter jadwal based on mode
let filteredJadwal = [];
if (currentScheduleMode === 1) {
// 7-day mode: show ALL schedules, mark which ones are active for this day
jadwal.forEach((j, i) => {
const days = j.days !== undefined ? j.days : 0x7F;
const isActiveToday = (days & (1 << currentDayTab)) !== 0;
filteredJadwal.push({ ...j, originalIndex: i, isActiveToday: isActiveToday });
});
} else {
// 1-day mode: show all with global enabled
filteredJadwal = jadwal.map((j, i) => ({ ...j, originalIndex: i, isActiveToday: j.enabled !== false }));
}
if (!filteredJadwal.length) {
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
filteredJadwal.forEach((j, displayIndex) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-slate-50';
tr.dataset.originalIndex = j.originalIndex;
// In 7-day mode, checkbox controls per-day activation (days bit)
// In 1-day mode, checkbox controls global enabled field
const checkboxTitle = currentScheduleMode === 1
? `Aktif di ${dayLabels[currentDayTab]}`
: 'Enabled';
tr.innerHTML = `
<td class="text-center text-slate-500 col-index">${displayIndex + 1}</td>
<td class="text-center"><input type="checkbox" data-enabled ${j.isActiveToday ? 'checked' : ''} title="${checkboxTitle}" 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="delByIndex(${j.originalIndex})" class="btn btn-red small">✕</button>
</div>
</td>`;
body.appendChild(tr);
});
}
function switchDayTab(day) {
currentDayTab = day;
// Update active state
document.querySelectorAll('.day-tab').forEach(btn => {
btn.classList.remove('active');
if (parseInt(btn.dataset.day) === day) {
btn.classList.add('active');
}
});
render();
}
function tambah() {
if (!isLoggedIn) return;
if (jadwal.length >= 20) return alert('Maksimal 20 jadwal');
// In 7-day mode, set only current day; in 1-day mode, set all days
const days = currentScheduleMode === 1 ? (1 << currentDayTab) : 0x7F;
jadwal.push({ jam: 7, menit: 0, track: 1, desc: 'Bel Masuk', enabled: true, days: days });
render();
}
function delByIndex(originalIndex) {
if (!isLoggedIn) return;
if (confirm('Hapus jadwal ini?')) {
jadwal.splice(originalIndex, 1);
render();
}
}
// Keep old del function for compatibility
function del(i) {
delByIndex(i);
}
async function simpan() {
if (!isLoggedIn) return;
const rows = document.querySelectorAll('#body tr');
// Update jadwal array based on displayed rows
rows.forEach(tr => {
const originalIndex = parseInt(tr.dataset.originalIndex);
if (isNaN(originalIndex) || originalIndex >= jadwal.length) return;
const enabledCb = tr.querySelector('input[data-enabled]');
const numInputs = tr.querySelectorAll('input[type="number"]');
const textInput = tr.querySelector('input[type="text"]');
if (numInputs.length >= 3 && enabledCb) {
jadwal[originalIndex].jam = parseInt(numInputs[0].value || 0);
jadwal[originalIndex].menit = parseInt(numInputs[1].value || 0);
jadwal[originalIndex].track = parseInt(numInputs[2].value || 1);
jadwal[originalIndex].desc = textInput ? textInput.value : '';
if (currentScheduleMode === 1) {
// 7-day mode: checkbox controls per-day bit in days field
let currentDays = jadwal[originalIndex].days !== undefined ? jadwal[originalIndex].days : 0x7F;
if (enabledCb.checked) {
// Add current day bit
currentDays |= (1 << currentDayTab);
} else {
// Remove current day bit
currentDays &= ~(1 << currentDayTab);
}
jadwal[originalIndex].days = currentDays;
// Keep enabled always true in 7-day mode (days field controls per-day)
jadwal[originalIndex].enabled = true;
} else {
// 1-day mode: checkbox controls global enabled
jadwal[originalIndex].enabled = enabledCb.checked;
}
}
});
await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jadwal)
});
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;
document.getElementById('testTrack').value = d.testTrack || 1000;
if (d.user) document.getElementById('userInput').placeholder = d.user;
// Load schedule mode
currentScheduleMode = d.scheduleMode || 0;
document.getElementById('scheduleMode').value = currentScheduleMode;
updateScheduleModeUI();
render(); // Re-render to show/hide days column
}
function updateScheduleModeUI() {
const skipSundayContainer = document.getElementById('skipSundayContainer');
const hint = document.getElementById('scheduleModeHint');
if (currentScheduleMode === 1) {
// Mode 7-hari: hide skipSunday option
skipSundayContainer.classList.add('hidden');
hint.textContent = 'Mode 7 Hari: Pilih hari aktif untuk setiap jadwal di tabel Jadwal.';
} else {
// Mode 1-hari: show skipSunday option
skipSundayContainer.classList.remove('hidden');
hint.textContent = 'Mode 1 Hari: Jadwal sama setiap hari. Centang opsi di atas untuk menonaktifkan bel di hari Minggu.';
}
}
function onScheduleModeChange() {
currentScheduleMode = parseInt(document.getElementById('scheduleMode').value);
updateScheduleModeUI();
render(); // Re-render to show/hide days column
}
async function saveScheduleMode() {
const mode = parseInt(document.getElementById('scheduleMode').value);
const skip = document.getElementById('skipSunday').checked;
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scheduleMode: mode, skipSunday: skip })
});
currentScheduleMode = mode;
alert('Mode Jadwal Disimpan!');
}
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 saveTestTrack() {
const track = parseInt(document.getElementById('testTrack').value) || 1000;
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ testTrack: track })
});
alert('Track Test Disimpan: ' + track);
}
async function previewTestTrack() {
const track = parseInt(document.getElementById('testTrack').value) || 1000;
await fetch(`/api/play?track=${track}`);
}
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>