Files
bel-sekolah-esp32/data/setting.html
2025-11-24 08:32:53 +08:00

423 lines
16 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.
<!-- 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>