adding alert < 2025
This commit is contained in:
422
data/setting.html
Normal file
422
data/setting.html
Normal 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>
|
||||
Reference in New Issue
Block a user