adding alert < 2025

This commit is contained in:
2025-11-24 08:32:53 +08:00
commit 94e3319c59
11 changed files with 1493 additions and 0 deletions

422
data/setting.html Normal file
View File

@@ -0,0 +1,422 @@
<!-- data/setting.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>
/* Inline Tailwind CSS for offline compatibility */
.bg-slate-50 { --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); }
.min-h-screen { min-height: 100vh; }
.p-6 { padding: 1.5rem; }
.max-w-3xl { max-width: 48rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); }
.p-4 { padding: 1rem; }
.rounded-xl { border-radius: 0.75rem; }
.shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-width 0px), var(--tw-ring-shadow), var(--tw-shadow); }
.mb-6 { margin-bottom: 1.5rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.font-bold { font-weight: 700; }
.text-blue-600 { --tw-text-opacity: 1; color: rgb(37 99 235 / var(--tw-text-opacity)); }
.text-center { text-align: center; }
.mb-4 { margin-bottom: 1rem; }
.block { display: block; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.font-medium { font-weight: 500; }
.text-slate-700 { --tw-text-opacity: 1; color: rgb(51 65 85 / var(--tw-text-opacity)); }
.mb-2 { margin-bottom: 0.5rem; }
.w-full { width: 100%; }
.border { border-width: 1px; border-style: solid; border-color: rgb(203 213 225); }
.rounded { border-radius: 0.25rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.hidden { display: none; }
.text-red-600 { --tw-text-opacity: 1; color: rgb(220 38 38 / var(--tw-text-opacity)); }
.text-slate-600 { --tw-text-opacity: 1; color: rgb(71 85 105 / var(--tw-text-opacity)); }
.font-semibold { font-weight: 600; }
.w-12 { width: 3rem; }
.bg-slate-100 { --tw-bg-opacity: 1; background-color: rgb(241 245 249 / var(--tw-bg-opacity)); }
.p-2 { padding: 0.5rem; }
.text-slate-400 { --tw-text-opacity: 1; color: rgb(148 163 184 / var(--tw-text-opacity)); }
.flex { display: flex; }
.gap-2 { gap: 0.5rem; }
.btn { display: inline-block; padding: 0.5rem 0.9rem; border-radius: 8px; color: white; font-weight: 600; text-decoration: none; cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); }
.btn.small { padding: 0.25rem 0.5rem; font-size: 0.9rem; }
.btn.btn-green { background: #059669; }
.btn.btn-blue { background: #2563EB; }
.btn.btn-red { background: #DC2626; }
.btn.btn-gray { background: #6B7280; }
.btn.btn-purple { background: #7C3AED; }
.btn:hover { filter: brightness(0.95); }
.flex-1 { flex: 1 1 0%; }
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid { display: grid; }
.gap-3 { gap: 0.75rem; }
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-slate-500 { --tw-text-opacity: 1; color: rgb(100 116 139 / var(--tw-text-opacity)); }
.overflow-x-auto { overflow-x: auto; }
.whitespace-nowrap { white-space: nowrap; }
/* Responsive */
@media (max-width: 768px) {
.md\:grid-cols-2 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
}
</style>
</head>
<body class="bg-slate-50 min-h-screen p-6">
<div class="max-w-3xl mx-auto">
<!-- Login Form -->
<div id="loginForm" class="bg-white p-6 rounded-xl shadow mb-6">
<h1 class="text-2xl font-bold text-blue-600 text-center mb-4">🔐 Login Admin</h1>
<form onsubmit="login(event)">
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 mb-2">Username</label>
<input id="loginUser" type="text" class="w-full border rounded px-3 py-2" placeholder="Masukkan username" required>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-slate-700 mb-2">Password</label>
<input id="loginPass" type="password" class="w-full border rounded px-3 py-2" placeholder="Masukkan password" required>
</div>
<button type="submit" class="w-full btn btn-blue">Masuk</button>
</form>
<div id="loginError" class="mt-4 text-red-600 text-sm text-center hidden"></div>
</div>
<!-- Settings Content (hidden initially) -->
<div id="settingsContent" class="hidden">
<header class="text-center mb-6">
<h1 class="text-2xl font-bold text-blue-600">⚙️ Pengaturan Bel Sekolah</h1>
<p class="text-sm text-slate-600">Kelola jadwal, nama sekolah, dan kredensial admin</p>
</header>
<section class="bg-white p-4 rounded-xl shadow mb-6">
<h2 class="font-semibold mb-2">📋 Jadwal (maks 20)</h2>
<div class="overflow-x-auto">
<table class="w-full text-sm whitespace-nowrap" id="tbl">
<thead class="bg-slate-100">
<tr><th class="p-2">#</th><th class="p-2">Aktif</th><th class="p-2">Jam</th><th class="p-2">Menit</th><th class="p-2 w-12">Track</th><th class="p-2">Deskripsi</th><th class="p-2">Aksi</th></tr>
</thead>
<tbody id="body"></tbody>
</table>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button onclick="tambah()" class="btn btn-green" title="Tambah"></button>
<button onclick="simpan()" class="btn btn-blue" title="Simpan Semua">💾</button>
<button onclick="stopBel()" class="btn btn-red" title="Stop"></button>
<a href="/" class="btn btn-gray" title="Kembali">🏠</a>
</div>
</section>
<section class="bg-white p-4 rounded-xl shadow mb-6">
<h2 class="font-semibold mb-2">🏫 Nama Sekolah</h2>
<div class="flex gap-2">
<input id="schoolName" class="flex-1 border rounded px-3 py-2" placeholder="Nama sekolah">
<button onclick="saveName()" class="btn btn-blue">💾 Simpan</button>
</div>
</section>
<section class="bg-white p-4 rounded-xl shadow mb-6">
<h2 class="font-semibold mb-2">📅 Pengaturan Hari Minggu</h2>
<div class="flex items-center gap-2">
<input type="checkbox" id="skipSunday" class="w-4 h-4">
<label for="skipSunday" class="text-sm text-slate-700">Jangan jalankan jadwal pada hari Minggu</label>
<button onclick="saveSkipSunday()" class="btn btn-blue">💾 Simpan</button>
</div>
</section>
<section class="bg-white p-4 rounded-xl shadow mb-6">
<h2 class="font-semibold mb-2">🕒 Sinkronisasi Waktu</h2>
<p class="text-sm text-slate-600 mb-3">Sinkronisasi waktu RTC dengan waktu browser</p>
<button onclick="syncTime()" class="btn btn-purple">🕒 Sync Waktu</button>
</section>
<section class="bg-white p-4 rounded-xl shadow mb-6">
<h2 class="font-semibold mb-2">🔐 Ganti Login Admin</h2>
<div class="grid md:grid-cols-2 gap-3 mb-3">
<input id="userInput" type="text" class="border rounded px-3 py-2" placeholder="Username baru">
<input id="passInput" type="password" class="border rounded px-3 py-2" placeholder="Password baru">
</div>
<div class="flex gap-2">
<button onclick="changeAdmin()" class="btn btn-purple">💾 Simpan Login Baru</button>
<a id="whatsappLink" href="https://wa.me/628113936644?text=Perlu%20Bantuan%3F" target="_blank" class="btn btn-green">💬 Perlu Bantuan?</a>
</div>
</section>
<section class="bg-white p-4 rounded-xl shadow mb-6">
<h2 class="font-semibold mb-2">💾 Backup & Restore</h2>
<p class="text-sm text-slate-600 mb-3">Cadangkan atau pulihkan pengaturan dan jadwal</p>
<div class="flex gap-2">
<button onclick="backup()" class="btn btn-blue">⬇️ Backup</button>
<input type="file" id="restoreFile" accept=".json" class="hidden">
<button onclick="restore()" class="btn btn-blue">⬆️ Restore</button>
</div>
</section>
<footer class="text-center text-xs text-slate-500">© Wartana 2025 — Bel Sekolah Otomatis</footer>
</div>
</div>
<script>
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) {
showLoginError('Masukkan username dan password');
return;
}
try {
const response = await fetch('/api/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user, pass })
});
const result = await response.json();
if (result.valid) {
isLoggedIn = true;
document.getElementById('loginForm').classList.add('hidden');
document.getElementById('settingsContent').classList.remove('hidden');
loadJadwal();
loadConfig();
} else {
showLoginError('Username atau password salah');
}
} catch (error) {
showLoginError('Terjadi kesalahan. Coba lagi.');
}
}
function showLoginError(message) {
const errorEl = document.getElementById('loginError');
errorEl.textContent = message;
errorEl.classList.remove('hidden');
}
async function loadJadwal() {
if (!isLoggedIn) return;
const r = await fetch('/api/jadwal');
const d = await r.json();
jadwal = d || [];
render();
}
function render(){
if (!isLoggedIn) return;
const body = document.getElementById('body');
body.innerHTML = '';
if (!jadwal.length) {
body.innerHTML = `<tr><td colspan="7" class="p-3 text-center text-slate-400">Belum ada jadwal</td></tr>`;
return;
}
jadwal.forEach((j,i) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="p-2 text-center">${i+1}</td>
<td class="p-2 text-center"><input type="checkbox" ${j.enabled !== false ? 'checked' : ''}></td>
<td class="p-2"><input type="number" min="0" max="23" value="${j.jam}"></td>
<td class="p-2"><input type="number" min="0" max="59" value="${j.menit}"></td>
<td class="p-2"><input type="number" min="1" value="${j.track || j.trackStart || 1}"></td>
<td class="p-2"><input type="text" value="${j.desc || ''}"></td>
<td class="p-2 text-center">
<button onclick="preview(${j.track || j.trackStart || 1})" class="btn btn-green small">▶</button>
<button onclick="del(${i})" class="btn btn-red small">❌</button>
</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;
jadwal.splice(i,1);
render();
}
async function simpan(){
if (!isLoggedIn) return;
// gather from table inputs
const rows = document.querySelectorAll('#body tr');
const arr = [];
rows.forEach(r => {
const inputs = r.querySelectorAll('input');
const checkbox = r.querySelector('input[type="checkbox"]');
if (inputs.length >= 5) { // checkbox + 4 inputs
const trackVal = parseInt(inputs[3].value || 1); // inputs[3] is track
arr.push({
jam: parseInt(inputs[1].value || 0), // inputs[1] is jam
menit: parseInt(inputs[2].value || 0), // inputs[2] is menit
track: trackVal, // inputs[3] is track
desc: inputs[4].value || '', // inputs[4] is desc
enabled: checkbox ? checkbox.checked : true
});
}
});
await fetch('/api/save', {
method:'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(arr)
});
alert('✅ Jadwal disimpan');
loadJadwal();
}
async function saveName(){
if (!isLoggedIn) return;
const name = document.getElementById('schoolName').value.trim();
if (!name) return alert('Isi nama sekolah');
await fetch('/api/config', {
method:'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({schoolName: name})
});
alert('✅ Nama sekolah disimpan');
}
async function saveSkipSunday(){
if (!isLoggedIn) return;
const skip = document.getElementById('skipSunday').checked;
await fetch('/api/config', {
method:'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({skipSunday: skip})
});
alert('✅ Pengaturan hari Minggu disimpan');
}
async function changeAdmin(){
if (!isLoggedIn) return;
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 diperbarui. Silakan login ulang.');
location.reload();
} else alert('❌ Gagal');
}
async function preview(track){
if (!isLoggedIn) return;
await fetch(`/api/play?track=${track}`);
}
async function stopBel(){
if (!isLoggedIn) return;
await fetch('/api/stop');
}
async function loadConfig(){
if (!isLoggedIn) return;
const r = await fetch('/api/config');
const d = await r.json();
document.getElementById('schoolName').value = d.schoolName || '';
document.getElementById('userInput').placeholder = d.user || 'admin';
document.getElementById('skipSunday').checked = d.skipSunday || false;
}
async function loadOTAInfo(){
if (!isLoggedIn) return;
try {
const response = await fetch('/api/ota');
const data = await response.json();
document.getElementById('otaHostname').textContent = data.hostname;
alert(`OTA Info:\nHostname: ${data.hostname}\nPassword: ${data.password}\n\nGunakan Arduino IDE atau PlatformIO untuk upload OTA.`);
} catch (error) {
alert('❌ Gagal memuat info OTA');
}
}
async function syncTime(){
if (!isLoggedIn) return;
const now = new Date();
// Use UTC time (subtract timezone offset to get UTC epoch)
const epoch = Math.floor((now.getTime() - now.getTimezoneOffset() * 60000) / 1000);
try {
const response = await fetch('/api/settime', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ epoch })
});
const result = await response.json();
if (result.ok) {
alert('✅ Waktu berhasil disinkronisasi');
} else {
alert('❌ Gagal sinkronisasi waktu');
}
} catch(e) {
console.error(e);
alert('❌ Error sinkronisasi waktu');
}
}
async function backup(){
if (!isLoggedIn) return;
try {
const response = await fetch('/api/backup');
if (!response.ok) throw new Error('Backup failed');
const data = await response.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 = 'bel-sekolah-backup.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('✅ Backup berhasil diunduh');
} catch (error) {
alert('❌ Gagal backup: ' + error.message);
}
}
async function restore(){
if (!isLoggedIn) return;
const fileInput = document.getElementById('restoreFile');
fileInput.click();
fileInput.onchange = async () => {
const file = fileInput.files[0];
if (!file) return;
try {
const text = await file.text();
const cleanText = text.replace(/^\uFEFF/, ''); // remove BOM if present
const data = JSON.parse(cleanText);
const response = await fetch('/api/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Restore failed');
const result = await response.json();
if (result.ok) {
alert('✅ Restore berhasil. Halaman akan dimuat ulang.');
location.reload();
} else {
alert('❌ Restore gagal');
}
} catch (error) {
alert('❌ Gagal restore: ' + error.message);
}
};
}
</script>
</body>
</html>