From 034ad6f9a38be1ccda8467f33c8b50e48492f2d0 Mon Sep 17 00:00:00 2001 From: wwartana Date: Sun, 21 Dec 2025 23:10:59 +0800 Subject: [PATCH] Jadwal 2 mode, reset di di box, test button ampli on, dan bel manual --- data/index.html | 496 +++++++++++++++++++++++++++++++++------------- data/setting.html | 259 +++++++++++++++++++++--- src/main.cpp | 43 +++- 3 files changed, 622 insertions(+), 176 deletions(-) diff --git a/data/index.html b/data/index.html index 7ff67d8..af26d68 100644 --- a/data/index.html +++ b/data/index.html @@ -1,65 +1,245 @@ + Bel Sekolah +
@@ -120,93 +327,96 @@
- + - + + \ No newline at end of file diff --git a/data/setting.html b/data/setting.html index ffe099d..82d3659 100644 --- a/data/setting.html +++ b/data/setting.html @@ -515,6 +515,35 @@ .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; + } @@ -584,7 +613,7 @@
-
+

Jadwal

@@ -592,6 +621,26 @@
+ + +
@@ -610,7 +659,7 @@
@@ -629,14 +678,33 @@
-

📅 Hari Libur

-
+

📅 Mode Jadwal

+
+ + +
+
-
- +

Mode 1 Hari: Jadwal sama setiap hari. Centang + opsi di atas untuk menonaktifkan bel di hari Minggu.

+
+ +
+
+ +
+

🔔 Track Bel Manual

+

Track MP3 yang diputar saat tombol TEST ditekan lama (2 detik)

+
+ + +
@@ -746,6 +814,10 @@ // --- 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(); @@ -792,20 +864,50 @@ function render() { const body = document.getElementById('body'); const empty = document.getElementById('emptyJadwal'); + const dayTabsContainer = document.getElementById('dayTabsContainer'); body.innerHTML = ''; - if (!jadwal.length) { + // 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'); - jadwal.forEach((j, i) => { + 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 = ` - ${i + 1} - + ${displayIndex + 1} + @@ -813,49 +915,91 @@
- +
`; 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 }); + 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 del(i) { + 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(i, 1); + 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'); - 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 - }); + + // 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(arr) + body: JSON.stringify(jadwal) }); alert('✅ Jadwal Tersimpan!'); loadJadwal(); @@ -872,7 +1016,49 @@ 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() { @@ -897,6 +1083,21 @@ 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); diff --git a/src/main.cpp b/src/main.cpp index 200b44e..3f32a53 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -54,8 +54,12 @@ String adminUser = "admin"; String adminPass = "sekolah123"; String apName; bool skipSunday = false; +uint16_t testTrack = 1000; // Default test track for manual bell +uint8_t scheduleMode = 0; // 0 = 1-hari (seragam), 1 = 7-hari (per hari) -struct Sch { uint8_t jam; uint8_t menit; uint16_t track; String desc; bool enabled; uint32_t lastExecuted; }; +// days: bit-field untuk hari aktif (bit 0=Minggu, 1=Senin, 2=Selasa, 3=Rabu, 4=Kamis, 5=Jumat, 6=Sabtu) +// Default 0x7F = semua hari aktif (0111 1111) +struct Sch { uint8_t jam; uint8_t menit; uint16_t track; String desc; bool enabled; uint8_t days; uint32_t lastExecuted; }; Sch schedules[MAX_SCHEDULES]; uint8_t scheduleCount = 0; @@ -147,6 +151,8 @@ void loadConfig() { d["user"] = adminUser; d["pass"] = adminPass; d["skipSunday"] = skipSunday; + d["testTrack"] = testTrack; + d["scheduleMode"] = scheduleMode; File f = LittleFS.open("/config.json", "w"); if (f) { serializeJson(d, f); f.close(); } return; @@ -161,6 +167,8 @@ void loadConfig() { if (d.containsKey("user")) adminUser = d["user"].as(); if (d.containsKey("pass")) adminPass = d["pass"].as(); if (d.containsKey("skipSunday")) skipSunday = d["skipSunday"]; + if (d.containsKey("testTrack")) testTrack = d["testTrack"] | 1000; + if (d.containsKey("scheduleMode")) scheduleMode = d["scheduleMode"] | 0; } } @@ -170,6 +178,8 @@ void saveConfig() { d["user"] = adminUser; d["pass"] = adminPass; d["skipSunday"] = skipSunday; + d["testTrack"] = testTrack; + d["scheduleMode"] = scheduleMode; File f = LittleFS.open("/config.json", "w"); if (f) { serializeJson(d, f); f.close(); } } @@ -197,6 +207,7 @@ void loadSchedules() { schedules[i].track = arr[i]["track"] | 1; schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as(); schedules[i].enabled = arr[i]["enabled"] | true; + schedules[i].days = arr[i]["days"] | 0x7F; // default semua hari aktif } } @@ -210,6 +221,7 @@ void saveSchedules() { o["track"] = schedules[i].track; o["desc"] = schedules[i].desc; o["enabled"] = schedules[i].enabled; + o["days"] = schedules[i].days; } File f = LittleFS.open("/schedule.json", "w"); if (f) { serializeJson(doc, f); f.close(); } @@ -319,6 +331,7 @@ void initWebServer() { o["track"] = schedules[i].track; o["desc"] = schedules[i].desc; o["enabled"] = schedules[i].enabled; + o["days"] = schedules[i].days; } String out; serializeJson(d, out); req->send(200, "application/json", out); @@ -339,6 +352,7 @@ void initWebServer() { schedules[i].track = arr[i]["track"] | 1; schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as(); schedules[i].enabled = arr[i]["enabled"] | true; + schedules[i].days = arr[i]["days"] | 0x7F; } saveSchedules(); req->send(200, "application/json", "{\"ok\":true}"); @@ -351,6 +365,8 @@ void initWebServer() { d["schoolName"] = schoolName; d["user"] = adminUser; d["skipSunday"] = skipSunday; + d["testTrack"] = testTrack; + d["scheduleMode"] = scheduleMode; String out; serializeJson(d, out); req->send(200, "application/json", out); }); @@ -362,6 +378,8 @@ void initWebServer() { if (deserializeJson(d, data, len)) { req->send(400); return; } if (d.containsKey("schoolName")) schoolName = d["schoolName"].as(); if (d.containsKey("skipSunday")) skipSunday = d["skipSunday"]; + if (d.containsKey("testTrack")) testTrack = d["testTrack"] | 1000; + if (d.containsKey("scheduleMode")) scheduleMode = d["scheduleMode"] | 0; saveConfig(); req->send(200, "application/json", "{\"ok\":true}"); broadcastStatus(); @@ -423,6 +441,7 @@ void initWebServer() { doc["user"] = adminUser; doc["pass"] = adminPass; doc["skipSunday"] = skipSunday; + doc["scheduleMode"] = scheduleMode; JsonArray arr = doc.createNestedArray("schedules"); for (uint8_t i = 0; i < scheduleCount; ++i) { JsonObject o = arr.createNestedObject(); @@ -431,6 +450,7 @@ void initWebServer() { o["track"] = schedules[i].track; o["desc"] = schedules[i].desc; o["enabled"] = schedules[i].enabled; + o["days"] = schedules[i].days; } String out; serializeJson(doc, out); @@ -473,6 +493,7 @@ void initWebServer() { if (doc.containsKey("schoolName")) schoolName = doc["schoolName"].as(); if (doc.containsKey("user")) adminUser = doc["user"].as(); if (doc.containsKey("pass")) adminPass = doc["pass"].as(); + if (doc.containsKey("scheduleMode")) scheduleMode = doc["scheduleMode"] | 0; saveConfig(); // Update schedules if (doc.containsKey("schedules")) { @@ -485,6 +506,7 @@ void initWebServer() { schedules[i].track = arr[i]["track"] | 1; schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as(); schedules[i].enabled = arr[i]["enabled"] | true; + schedules[i].days = arr[i]["days"] | 0x7F; } saveSchedules(); } @@ -533,6 +555,7 @@ void initWebServer() { doc["user"] = adminUser; doc["pass"] = adminPass; doc["skipSunday"] = skipSunday; + doc["scheduleMode"] = scheduleMode; JsonArray arr = doc.createNestedArray("schedules"); for (uint8_t i = 0; i < scheduleCount; ++i) { JsonObject o = arr.createNestedObject(); @@ -541,6 +564,7 @@ void initWebServer() { o["track"] = schedules[i].track; o["desc"] = schedules[i].desc; o["enabled"] = schedules[i].enabled; + o["days"] = schedules[i].days; } File f = LittleFS.open(filename, "w"); if (f) { @@ -587,6 +611,7 @@ void initWebServer() { if (doc.containsKey("user")) adminUser = doc["user"].as(); if (doc.containsKey("pass")) adminPass = doc["pass"].as(); if (doc.containsKey("skipSunday")) skipSunday = doc["skipSunday"]; + if (doc.containsKey("scheduleMode")) scheduleMode = doc["scheduleMode"] | 0; saveConfig(); if (doc.containsKey("schedules")) { @@ -599,6 +624,7 @@ void initWebServer() { schedules[i].track = arr[i]["track"] | 1; schedules[i].desc = arr[i]["desc"].isNull() ? "" : arr[i]["desc"].as(); schedules[i].enabled = arr[i]["enabled"] | true; + schedules[i].days = arr[i]["days"] | 0x7F; } saveSchedules(); } @@ -798,8 +824,8 @@ void loop(){ // Process TEST_BUTTON long press: play test MP3 if (testBtnLongPressPending) { testBtnLongPressPending = false; - Serial.println("[BUTTON] Long press detected: playing track 1000"); - playTrack(1000, "Manual Test"); + Serial.printf("[BUTTON] Long press detected: playing test track %d\n", testTrack); + playTrack(testTrack, "Manual Test"); } // Process RESET_BUTTON long press: reset admin credentials @@ -851,9 +877,18 @@ void loop(){ // schedule loop: for each schedule, compute today's target unix if (now.year() >= 2025) { // only run schedules if date >= 2025 + uint8_t today = now.dayOfTheWeek(); // 0=Minggu, 1=Senin, ..., 6=Sabtu for (uint8_t i=0;i