SMKBK1 commited on
Commit
a4e39c6
·
verified ·
1 Parent(s): df091e0

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +29 -1510
index.html CHANGED
@@ -5,52 +5,26 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>SMK Bandar Kuching No. 1 - Attendance & Roster System</title>
7
  <!-- Tailwind CSS -->
8
- <script src="https://cdn.tailwindcss.com">
9
- </script>
10
  <!-- Lucide Icons -->
11
- <script src="https://unpkg.com/lucide@latest">
12
- </script>
13
  <!-- Inter Font -->
14
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
15
  <!-- Chart.js for Visual Analytics -->
16
- <script src="https://cdn.jsdelivr.net/npm/chart.js">
17
- </script>
18
  <style>
19
- body {
20
- font-family: 'Inter', sans-serif;
21
- }
22
- .status-btn {
23
- transition: all 0.2s ease-in-out;
24
- }
25
- .status-btn:active {
26
- transform: scale(0.95);
27
- }
28
  /* Custom scrollbar styling */
29
- ::-webkit-scrollbar {
30
- width: 6px;
31
- height: 6px;
32
- }
33
- ::-webkit-scrollbar-track {
34
- background: #f1f5f9;
35
- }
36
- ::-webkit-scrollbar-thumb {
37
- background: #cbd5e1;
38
- border-radius: 4px;
39
- }
40
- ::-webkit-scrollbar-thumb:hover {
41
- background: #94a3b8;
42
- }
43
  @media print {
44
- .no-print {
45
- display: none !important;
46
- }
47
- .print-only {
48
- display: block !important;
49
- }
50
- body {
51
- background: white !important;
52
- color: black !important;
53
- }
54
  }
55
  </style>
56
  </head>
@@ -119,7 +93,7 @@
119
 
120
  <!-- TAB 1: ATTENDANCE TRACKER -->
121
  <div id="tab-tracker" class="space-y-6">
122
-
123
  <!-- Controls Form -->
124
  <div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-5 no-print space-y-4">
125
  <div class="flex items-center justify-between border-b border-slate-100 pb-3 cursor-pointer select-none" onclick="toggleControlsSection()">
@@ -135,7 +109,7 @@
135
 
136
  <div id="controls-body" class="space-y-4 transition-all duration-300">
137
  <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-6 gap-3">
138
-
139
  <!-- Class Selection -->
140
  <div>
141
  <div class="flex items-center justify-between mb-1.5">
@@ -204,7 +178,7 @@
204
  </div>
205
  </div>
206
 
207
- <!-- End Time (extended to 16:00) -->
208
  <div>
209
  <label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1.5">6. End Time</label>
210
  <div class="relative">
@@ -564,7 +538,7 @@
564
 
565
  <!-- Detailed Analysis Tables -->
566
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-2">
567
-
568
  <!-- Pending Submission Table -->
569
  <div class="border border-amber-200 bg-amber-50/20 rounded-xl p-4 space-y-3">
570
  <div class="flex items-center justify-between border-b border-amber-200 pb-2">
@@ -705,7 +679,7 @@
705
  </div>
706
  </div>
707
  <p class="text-xs text-slate-600 leading-relaxed">
708
- Are you sure you want to permanently delete all attendance logs and historical sessions?
709
  </p>
710
  <div class="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
711
  <button type="button" id="btn-clear-logs-cancel" class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold text-xs rounded-xl">Cancel</button>
@@ -818,12 +792,12 @@
818
  function subscribeToLiveSettings() {
819
  if (!currentUser) return;
820
  const settingsDoc = doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'rosters_v1');
821
-
822
  onSnapshot(settingsDoc, (docSnap) => {
823
  if (docSnap.exists()) {
824
  const data = docSnap.data();
825
  let needsRefresh = false;
826
-
827
  if (data.rosters) {
828
  window.safeSetItem('smk_custom_rosters_v3', JSON.stringify(data.rosters));
829
  window.rosters = data.rosters;
@@ -843,7 +817,7 @@
843
  window.SUBJECT_LIST = data.subjects;
844
  needsRefresh = true;
845
  }
846
-
847
  if (needsRefresh && typeof window.refreshAllUI === 'function') {
848
  window.refreshAllUI();
849
  }
@@ -856,7 +830,7 @@
856
  window.saveSessionToCloud = async function(sessionRecord) {
857
  if (!currentUser) return;
858
  try {
859
- const docId = `session_${sessionRecord.id}_${sessionRecord.class}_${sessionRecord.startTime.replace(':', '')}`;
860
  const docRef = doc(db, 'artifacts', appId, 'public', 'data', 'sessions', docId);
861
  await setDoc(docRef, sessionRecord);
862
  } catch (err) {
@@ -867,7 +841,7 @@
867
  window.deleteSessionFromCloud = async function(sessionRecord) {
868
  if (!currentUser) return;
869
  try {
870
- const docId = `session_${sessionRecord.id}_${sessionRecord.class}_${sessionRecord.startTime.replace(':', '')}`;
871
  const docRef = doc(db, 'artifacts', appId, 'public', 'data', 'sessions', docId);
872
  await deleteDoc(docRef);
873
  } catch (err) {
@@ -879,9 +853,9 @@
879
  if (!currentUser) return;
880
  try {
881
  const docRef = doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'rosters_v1');
882
- await setDoc(docRef, {
883
- rosters: rostersData,
884
- classes: classesData || window.ALL_CLASSES,
885
  teachers: teachersData || window.MASTER_TEACHER_LIST,
886
  subjects: subjectsData || window.SUBJECT_LIST
887
  });
@@ -942,13 +916,8 @@
942
 
943
  window.SUBJECT_LIST = JSON.parse(window.safeGetItem('smk_subjects_v1', '["Bahasa Melayu", "English", "Mathematics", "Science", "Sejarah", "Geografi", "Pendidikan Islam", "Pendidikan Moral", "RBT", "ASK", "PJK", "Pendidikan Seni Visual", "Fizik", "Kimia", "Biologi", "Matematik Tambahan", "Prinsip Perakaunan", "Ekonomi"]'));
944
  window.MASTER_TEACHER_LIST = JSON.parse(window.safeGetItem('smk_teachers_v1', '["Cikgu Wee", "Cikgu Ahmad", "Cikgu Tan", "Cikgu Dayang", "Cikgu Ling"]'));
945
-
946
- // EXTENDED TIME SLOTS: Now includes times up to 16:00 (4:00 PM)
947
- window.TIME_SLOTS = [
948
- "07:00", "07:30", "08:00", "08:30", "09:00", "09:30",
949
- "10:00", "10:30", "11:00", "11:30", "12:00", "12:30",
950
- "13:00", "13:30", "14:00", "14:30", "15:00", "15:30", "16:00"
951
- ];
952
  window.ABSENCE_REASONS = ["Tanpa Sebab / Ponteng", "Sakit / Sijil Sakit (MC)", "Kebenaran Sekolah / Aktiviti", "Urusan Keluarga", "Masalah Pengangkutan", "Lain-lain"];
953
 
954
  // Global State variables
@@ -1025,1454 +994,4 @@
1025
  try { lucide.createIcons(); } catch (e) {}
1026
  try { populateDropdowns(); } catch (e) { console.warn('populateDropdowns', e); }
1027
  try { setCurrentDate(); } catch (e) { console.warn('setCurrentDate', e); }
1028
- try { setupEventDelegation(); } catch (e) { console.warn('setupEventDelegation', e); }
1029
- try { switchTab('history'); } catch (e) { console.warn('switchTab', e); }
1030
- // Visible proof that JS is alive
1031
- try {
1032
- const badge = document.getElementById('sync-text');
1033
- if (badge && badge.innerText === 'Live Cloud Active') {
1034
- // leave as is if cloud works
1035
- }
1036
- } catch (e) {}
1037
- }
1038
- window.initApp = initApp;
1039
-
1040
- // Event delegation — capture phase so it works even if something stops bubbling
1041
- function setupEventDelegation() {
1042
- if (window.__smkDelegationBound) return;
1043
- window.__smkDelegationBound = true;
1044
-
1045
- document.addEventListener('click', function(e) {
1046
- const t = e.target;
1047
-
1048
- // --- Delete single session (trash icon / ×) — instant, no confirm (Canvas-safe) ---
1049
- const delBtn = t.closest ? t.closest('[data-action="delete-session"], .js-delete-session') : null;
1050
- if (delBtn) {
1051
- e.preventDefault();
1052
- e.stopPropagation();
1053
- const id = delBtn.getAttribute('data-id');
1054
- if (id != null && id !== '') {
1055
- // Direct delete — no modal (modals can fail in Canvas sandboxes)
1056
- if (typeof window.performDeleteSession === 'function') {
1057
- window.performDeleteSession(id);
1058
- } else if (typeof performDeleteSession === 'function') {
1059
- performDeleteSession(id);
1060
- }
1061
- }
1062
- return;
1063
- }
1064
-
1065
- // --- Bulk delete ---
1066
- if (t.closest && t.closest('#btn-delete-selected')) {
1067
- e.preventDefault();
1068
- deleteSelectedSessions();
1069
- return;
1070
- }
1071
-
1072
- // --- Clear All trigger --- if (t.closest && t.closest('#btn-clear-all-logs')) {
1073
- e.preventDefault();
1074
- confirmClearAllLogs();
1075
- return;
1076
- }
1077
-
1078
- // --- Delete confirm modal ---
1079
- if (t.closest && t.closest('#btn-delete-confirm')) {
1080
- e.preventDefault();
1081
- executePendingDelete();
1082
- return;
1083
- }
1084
- if (t.id === 'delete-confirm-modal-backdrop' || (t.closest && t.closest('#btn-delete-cancel'))) {
1085
- e.preventDefault();
1086
- closeDeleteConfirmModal();
1087
- return;
1088
- }
1089
-
1090
- // --- Clear-all modal ---
1091
- if (t.closest && t.closest('#btn-clear-logs-confirm')) {
1092
- e.preventDefault();
1093
- executeClearAllLogs();
1094
- return;
1095
- }
1096
- if (t.closest && t.closest('#btn-clear-logs-cancel')) {
1097
- e.preventDefault();
1098
- closeClearLogsModal();
1099
- return;
1100
- }
1101
-
1102
- // --- Print ---
1103
- if (t.closest && t.closest('#btn-print-logs')) {
1104
- e.preventDefault();
1105
- try { window.print(); } catch (err) {
1106
- showToast('Print', 'Print is blocked in this environment. Use Export CSV instead.');
1107
- }
1108
- return;
1109
- }
1110
- }, true); // capture = true
1111
- }
1112
- window.setupEventDelegation = setupEventDelegation;
1113
-
1114
- // Boot immediately + on DOM ready (covers both normal browsers and Gemini Canvas)
1115
- if (document.readyState === 'loading') {
1116
- document.addEventListener('DOMContentLoaded', initApp);
1117
- } else {
1118
- initApp();
1119
- }
1120
- // Extra safety: run again shortly after in case Canvas rewrites DOM
1121
- setTimeout(function() {
1122
- window.__smkAppInitialized = false;
1123
- initApp();
1124
- }, 300);
1125
- setTimeout(function() {
1126
- window.__smkAppInitialized = false;
1127
- initApp();
1128
- }, 1000);
1129
-
1130
- function populateDropdowns() {
1131
- const classSelects = ['select-class', 'timetable-view-class', 'roster-manager-class-select'];
1132
- classSelects.forEach(id => {
1133
- const el = document.getElementById(id);
1134
- if (!el) return;
1135
- el.innerHTML = window.ALL_CLASSES.map(c => `<option value="${c}">${c}</option>`).join('');
1136
- });
1137
-
1138
- const subjectSelect = document.getElementById('select-subject');
1139
- if (subjectSelect) {
1140
- subjectSelect.innerHTML = window.SUBJECT_LIST.map(s => `<option value="${s}">${s}</option>`).join('');
1141
- }
1142
-
1143
- const teacherSelect = document.getElementById('select-teacher');
1144
- if (teacherSelect) {
1145
- teacherSelect.innerHTML = window.MASTER_TEACHER_LIST.map(t => `<option value="${t}">${t}</option>`).join('');
1146
- }
1147
-
1148
- const startSelect = document.getElementById('select-start-time');
1149
- const endSelect = document.getElementById('select-end-time');
1150
- if (startSelect && endSelect) {
1151
- startSelect.innerHTML = window.TIME_SLOTS.map(t => `<option value="${t}">${t}</option>`).join('');
1152
- endSelect.innerHTML = window.TIME_SLOTS.map(t => `<option value="${t}">${t}</option>`).join('');
1153
- if (window.TIME_SLOTS.length > 1) {
1154
- startSelect.value = window.TIME_SLOTS[0];
1155
- endSelect.value = window.TIME_SLOTS[1];
1156
- }
1157
- }
1158
- }
1159
-
1160
- function setCurrentDate() {
1161
- const todayISO = formatDateToISO(new Date());
1162
-
1163
- const dateInput = document.getElementById('input-date');
1164
- if (dateInput) dateInput.value = todayISO;
1165
-
1166
- const ttDateInput = document.getElementById('timetable-date');
1167
- if (ttDateInput) ttDateInput.value = todayISO;
1168
-
1169
- const analyticsDateInput = document.getElementById('analytics-date-input');
1170
- if (analyticsDateInput) analyticsDateInput.value = todayISO;
1171
-
1172
- onDateChange();
1173
- }
1174
-
1175
- function switchTab(tabId) {
1176
- const tabs = ['tracker', 'timetable', 'history', 'roster', 'analytics'];
1177
- tabs.forEach(t => {
1178
- const section = document.getElementById(`tab-${t}`);
1179
- if (section) section.classList.add('hidden');
1180
-
1181
- const btn = document.getElementById(`tab-btn-${t}`);
1182
- if (btn) {
1183
- btn.classList.remove('border-emerald-600', 'text-emerald-600');
1184
- btn.classList.add('border-transparent', 'text-slate-500');
1185
- }
1186
- });
1187
-
1188
- const activeSection = document.getElementById(`tab-${tabId}`);
1189
- if (activeSection) activeSection.classList.remove('hidden');
1190
-
1191
- const activeBtn = document.getElementById(`tab-btn-${tabId}`);
1192
- if (activeBtn) {
1193
- activeBtn.classList.remove('border-transparent', 'text-slate-500');
1194
- activeBtn.classList.add('border-emerald-600', 'text-emerald-600');
1195
- }
1196
-
1197
- if (tabId === 'tracker') {
1198
- onClassChange();
1199
- } else if (tabId === 'history') {
1200
- renderHistoryTable();
1201
- } else if (tabId === 'timetable') {
1202
- renderTimetableGrid();
1203
- } else if (tabId === 'roster') {
1204
- onRosterManagerClassChange();
1205
- } else if (tabId === 'analytics') {
1206
- renderAnalyticsDashboard();
1207
- }
1208
- }
1209
-
1210
- function onDateChange() {
1211
- const dateInputVal = document.getElementById('input-date').value;
1212
- const dateObj = parseDateString(dateInputVal) || new Date();
1213
- const dayName = getDayKeyFromDate(dateObj);
1214
-
1215
- document.getElementById('display-day-badge').innerText = dayName;
1216
- document.getElementById('current-date-display').innerText = formatDateToISO(dateObj);
1217
- updateBannerInfo();
1218
- }
1219
-
1220
- function onClassChange() {
1221
- const className = document.getElementById('select-class').value;
1222
- updateBannerInfo();
1223
-
1224
- window.currentRosterStudents = window.rosters[className] || Array.from({ length: 30 }, (_, i) => `Student ${i + 1}`);
1225
-
1226
- const total = window.currentRosterStudents.length;
1227
- const totalInput = document.getElementById('input-total-enrolled');
1228
- const presentInput = document.getElementById('input-present-count');
1229
-
1230
- if (totalInput) totalInput.value = total;
1231
- if (presentInput) presentInput.value = total;
1232
-
1233
- const container = document.getElementById('absentee-cards-container');
1234
- if (container) container.innerHTML = '';
1235
-
1236
- updateAttendanceCalc();
1237
- }
1238
-
1239
- function updateBannerInfo() {
1240
- const className = document.getElementById('select-class').value;
1241
- const subject = document.getElementById('select-subject').value;
1242
- const teacher = document.getElementById('select-teacher').value;
1243
- const startTime = document.getElementById('select-start-time').value;
1244
- const endTime = document.getElementById('select-end-time').value;
1245
- const day = document.getElementById('display-day-badge').innerText;
1246
-
1247
- document.getElementById('badge-class-name').innerText = className;
1248
- document.getElementById('banner-subject-title').innerText = subject;
1249
- document.getElementById('badge-teacher-name').innerHTML =
1250
- `<i data-lucide="user" class="w-3.5 h-3.5 text-emerald-400"></i> Teacher: ${teacher}`;
1251
- document.getElementById('banner-subject-details').innerHTML = `
1252
- <span><i data-lucide="clock" class="w-3.5 h-3.5 inline mr-1 text-teal-400"></i> Slot: ${startTime} - ${endTime}</span>
1253
- <span><i data-lucide="calendar" class="w-3.5 h-3.5 inline mr-1 text-teal-400"></i> Day: ${day}</span>
1254
- `;
1255
- lucide.createIcons();
1256
- }
1257
-
1258
- function changePresentCount(delta) {
1259
- const input = document.getElementById('input-present-count');
1260
- const total = parseInt(document.getElementById('input-total-enrolled').value) || 0;
1261
- let val = parseInt(input.value) || 0;
1262
- val += delta;
1263
-
1264
- if (val < 0) val = 0;
1265
- if (val > total) val = total;
1266
-
1267
- input.value = val;
1268
- updateAttendanceCalc();
1269
- }
1270
-
1271
- function updateAttendanceCalc() {
1272
- const total = parseInt(document.getElementById('input-total-enrolled').value) || 0;
1273
- let present = parseInt(document.getElementById('input-present-count').value) || 0;
1274
-
1275
- if (present > total) {
1276
- present = total;
1277
- document.getElementById('input-present-count').value = present;
1278
- }
1279
-
1280
- const absent = total - present;
1281
- const rate = total > 0 ? ((present / total) * 100).toFixed(1) : 0;
1282
-
1283
- const absentCountEl = document.getElementById('calc-absent-count');
1284
- const absentBadgeEl = document.getElementById('calc-absent-badge');
1285
- const rateBadgeEl = document.getElementById('rate-status-badge');
1286
-
1287
- if (absentCountEl) absentCountEl.innerText = absent;
1288
- if (absentBadgeEl) absentBadgeEl.innerText = `${absent} absent`;
1289
- if (rateBadgeEl) rateBadgeEl.innerText = `Rate: ${rate}%`;
1290
-
1291
- renderAbsenteeCards(absent);
1292
- }
1293
-
1294
- function renderAbsenteeCards(count) {
1295
- const container = document.getElementById('absentee-cards-container');
1296
- const section = document.getElementById('absentee-section');
1297
- if (!container || !section) return;
1298
-
1299
- document.getElementById('absentee-section-count').innerText = count;
1300
-
1301
- if (count <= 0) {
1302
- section.classList.add('hidden');
1303
- return;
1304
- }
1305
- section.classList.remove('hidden');
1306
-
1307
- const currentCount = container.children.length;
1308
- const reasonsHtml = window.ABSENCE_REASONS.map(r => `<option value="${r}">${r}</option>`).join('');
1309
- let studentsHtml = `<option value="">-- Select Student --</option>` + window.currentRosterStudents.map(s =>
1310
- `<option value="${s}">${s}</option>`).join('');
1311
-
1312
- if (count > currentCount) {
1313
- for (let i = currentCount; i < count; i++) {
1314
- const card = document.createElement('div');
1315
- card.className = "bg-rose-50/40 border border-rose-200 rounded-xl p-3 space-y-2.5 shadow-xs";
1316
- card.id = `absentee-card-${i}`;
1317
- card.innerHTML = `
1318
- <div class="flex items-center justify-between border-b border-rose-100 pb-1.5">
1319
- <span class="inline-block bg-rose-100 text-rose-800 text-[11px] font-bold px-2 py-0.5 rounded border border-rose-200">
1320
- Absentee #${i + 1}
1321
- </span>
1322
- </div>
1323
-
1324
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
1325
- <div class="space-y-1">
1326
- <label class="block text-[10px] font-bold uppercase tracking-wide text-slate-500">Student Name</label>
1327
- <select id="absentee-select-${i}" class="w-full bg-white border border-slate-300 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-800 focus:ring-2 focus:ring-rose-500 focus:border-rose-500 transition">
1328
- ${studentsHtml}
1329
- </select>
1330
- </div>
1331
-
1332
- <div class="space-y-1">
1333
- <label class="block text-[10px] font-bold uppercase tracking-wide text-slate-500">Reason</label>
1334
- <select id="absentee-reason-${i}" class="w-full bg-white border border-slate-300 rounded-lg px-2.5 py-1.5 text-xs font-bold text-rose-700 focus:ring-2 focus:ring-rose-500 focus:border-rose-500 transition">
1335
- ${reasonsHtml}
1336
- </select>
1337
- </div>
1338
- </div>
1339
-
1340
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
1341
- <div class="space-y-1">
1342
- <label class="block text-[10px] font-bold uppercase tracking-wide text-slate-500">Manual Name (Optional)</label>
1343
- <input type="text" id="absentee-name-${i}" placeholder="If not in list..." class="w-full bg-white border border-slate-300 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-800 focus:ring-2 focus:ring-rose-500 focus:border-rose-500 transition">
1344
- </div>
1345
-
1346
- <div class="space-y-1">
1347
- <label class="block text-[10px] font-bold uppercase tracking-wide text-slate-500">Remark / Notes</label>
1348
- <input type="text" id="absentee-remark-${i}" placeholder="Optional details..." class="w-full bg-white border border-slate-300 rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-800 focus:ring-2 focus:ring-rose-500 focus:border-rose-500 transition">
1349
- </div>
1350
- </div>
1351
- `;
1352
- container.appendChild(card);
1353
- }
1354
- } else if (count < currentCount) {
1355
- for (let i = currentCount - 1; i >= count; i--) {
1356
- container.removeChild(container.children[i]);
1357
- }
1358
- }
1359
- }
1360
-
1361
- function toggleControlsSection() {
1362
- const body = document.getElementById('controls-body');
1363
- const icon = document.getElementById('controls-toggle-icon');
1364
- const text = document.getElementById('controls-toggle-text');
1365
- if (!body) return;
1366
-
1367
- if (body.classList.contains('hidden')) {
1368
- body.classList.remove('hidden');
1369
- icon.classList.add('rotate-180');
1370
- text.innerText = "Collapse Setup";
1371
- } else {
1372
- body.classList.add('hidden');
1373
- icon.classList.remove('rotate-180');
1374
- text.innerText = "Expand Setup";
1375
- }
1376
- }
1377
-
1378
- function toggleAbsenteeSection() {
1379
- const container = document.getElementById('absentee-cards-container');
1380
- const icon = document.getElementById('absentee-toggle-icon');
1381
- const text = document.getElementById('absentee-toggle-text');
1382
- if (!container) return;
1383
-
1384
- if (container.classList.contains('hidden')) {
1385
- container.classList.remove('hidden');
1386
- icon.classList.add('rotate-180');
1387
- text.innerText = "Collapse";
1388
- } else {
1389
- container.classList.add('hidden');
1390
- icon.classList.remove('rotate-180');
1391
- text.innerText = "Expand";
1392
- }
1393
- }
1394
-
1395
- function saveAttendanceSession() {
1396
- updateAttendanceCalc();
1397
-
1398
- const rawDate = document.getElementById('input-date').value;
1399
- const parsedD = parseDateString(rawDate) || new Date();
1400
- const date = formatDateToISO(parsedD);
1401
- const dayKey = getDayKeyFromDate(parsedD);
1402
-
1403
- const className = document.getElementById('select-class').value;
1404
- const subject = document.getElementById('select-subject').value;
1405
- const teacher = document.getElementById('select-teacher').value;
1406
- const startTime = document.getElementById('select-start-time').value;
1407
- const endTime = document.getElementById('select-end-time').value;
1408
-
1409
- const total = parseInt(document.getElementById('input-total-enrolled').value) || 0;
1410
- const present = parseInt(document.getElementById('input-present-count').value) || 0;
1411
- const absent = total - present;
1412
- const rate = total > 0 ? ((present / total) * 100).toFixed(1) + '%' : '0%';
1413
-
1414
- if (!date || !className) {
1415
- showToast("Missing Fields", "Please ensure Date and Class are selected.");
1416
- return;
1417
- }
1418
-
1419
- let absenteesList = [];
1420
- for (let i = 0; i < absent; i++) {
1421
- const selectEl = document.getElementById(`absentee-select-${i}`);
1422
- const nameEl = document.getElementById(`absentee-name-${i}`);
1423
- const reasonEl = document.getElementById(`absentee-reason-${i}`);
1424
- const remarkEl = document.getElementById(`absentee-remark-${i}`);
1425
-
1426
- let sName = nameEl && nameEl.value.trim() ? nameEl.value.trim() : (selectEl && selectEl.value ? selectEl.value :
1427
- "Unknown Student");
1428
- let sReason = reasonEl ? reasonEl.value : "No Reason";
1429
-
1430
- if (remarkEl && remarkEl.value.trim()) {
1431
- sReason += ` (${remarkEl.value.trim()})`;
1432
- }
1433
-
1434
- absenteesList.push({ name: sName, reason: sReason });
1435
- }
1436
-
1437
- const sessionRecord = {
1438
- id: Date.now(),
1439
- date,
1440
- dayKey,
1441
- class: className,
1442
- subject,
1443
- teacher,
1444
- startTime,
1445
- endTime,
1446
- total,
1447
- present,
1448
- absent,
1449
- rate,
1450
- absentees: absenteesList
1451
- };
1452
-
1453
- window.savedSessions = [sessionRecord, ...window.savedSessions.filter(s => s.id !== sessionRecord.id)];
1454
- window.safeSetItem('smk_saved_sessions_v1', JSON.stringify(window.savedSessions));
1455
-
1456
- if (typeof window.saveSessionToCloud === 'function') {
1457
- window.saveSessionToCloud(sessionRecord);
1458
- }
1459
-
1460
- refreshAllDataViews();
1461
-
1462
- showToast("Session Saved", `Attendance for ${className} recorded successfully.`);
1463
- }
1464
-
1465
- function toggleLogAbsentee(id) {
1466
- if (window.expandedLogSessions.has(id)) {
1467
- window.expandedLogSessions.delete(id);
1468
- } else {
1469
- window.expandedLogSessions.add(id);
1470
- }
1471
- renderHistoryTable();
1472
- }
1473
-
1474
- // Search & Multi-Select Log Functions
1475
- function toggleSelectAllLogs(masterCheckbox) {
1476
- const checkboxes = document.querySelectorAll('.log-item-checkbox');
1477
- checkboxes.forEach(cb => {
1478
- cb.checked = masterCheckbox.checked;
1479
- const id = parseInt(cb.getAttribute('data-id'));
1480
- if (masterCheckbox.checked) {
1481
- window.selectedLogIds.add(id);
1482
- } else {
1483
- window.selectedLogIds.delete(id);
1484
- }
1485
- });
1486
- updateDeleteSelectedButton();
1487
- }
1488
-
1489
- function toggleLogSelection(id, cb) {
1490
- if (cb.checked) {
1491
- window.selectedLogIds.add(id);
1492
- } else {
1493
- window.selectedLogIds.delete(id);
1494
- }
1495
- updateDeleteSelectedButton();
1496
- }
1497
-
1498
- function updateDeleteSelectedButton() {
1499
- const btn = document.getElementById('btn-delete-selected');
1500
- const countEl = document.getElementById('selected-count');
1501
- if (!btn || !countEl) return;
1502
-
1503
- const count = window.selectedLogIds.size;
1504
- countEl.innerText = count;
1505
- if (count > 0) {
1506
- btn.classList.remove('hidden');
1507
- } else {
1508
- btn.classList.add('hidden');
1509
- }
1510
- }
1511
-
1512
- function renderHistoryTable() {
1513
- const tbody = document.getElementById('history-table-body');
1514
- if (!tbody) return;
1515
-
1516
- const searchVal = (document.getElementById('log-search-input')?.value || '').toLowerCase().trim();
1517
- const totalCountEl = document.getElementById('total-logs-count');
1518
- if (totalCountEl) totalCountEl.innerText = window.savedSessions.length;
1519
-
1520
- let filtered = window.savedSessions.filter(s => {
1521
- if (!searchVal) return true;
1522
- return (s.class || '').toLowerCase().includes(searchVal) ||
1523
- (s.subject || '').toLowerCase().includes(searchVal) ||
1524
- (s.teacher || '').toLowerCase().includes(searchVal) ||
1525
- (s.date || '').toLowerCase().includes(searchVal);
1526
- });
1527
-
1528
- const sorted = [...filtered].sort((a, b) => b.id - a.id);
1529
-
1530
- let html = '';
1531
- sorted.forEach(s => {
1532
- const isExpanded = window.expandedLogSessions.has(s.id);
1533
- const isChecked = window.selectedLogIds.has(s.id);
1534
- const absCount = s.absentees ? s.absentees.length : 0;
1535
- const visibleAbsentees = (absCount > 2 && !isExpanded) ? s.absentees.slice(0, 2) : s.absentees;
1536
-
1537
- const absenteeBadges = visibleAbsentees.map(ab => {
1538
- const reasonBadge = (ab.reason && ab.reason !== "N/A" && ab.reason !== "No Reason") ?
1539
- `<span class="opacity-80 font-normal ml-1 text-[10px]">(${ab.reason})</span>` :
1540
- '';
1541
- return `<span class="inline-block bg-rose-100 text-rose-800 border border-rose-200 px-2.5 py-1 rounded-lg text-[11px] font-bold mr-1.5 mb-1.5 shadow-xs leading-tight" title="${ab.reason || ''}">${ab.name}${reasonBadge}</span>`;
1542
- }).join('');
1543
-
1544
- let absDisplay = '';
1545
- if (s.absent <= 0 || absCount === 0) {
1546
- absDisplay = '<span class="text-slate-400 italic text-xs">- None -</span>';
1547
- } else {
1548
- absDisplay = absenteeBadges;
1549
- if (absCount > 2) {
1550
- if (!isExpanded) {
1551
- absDisplay +=
1552
- `<button onclick="toggleLogAbsentee(${s.id})" class="inline-flex items-center gap-1 text-[11px] font-bold text-rose-700 bg-rose-50 hover:bg-rose-100 border border-rose-200 px-2 py-0.5 rounded-lg transition mt-0.5 shadow-2xs">
1553
- +${absCount - 2} more <i data-lucide="chevron-down" class="w-3 h-3"></i>
1554
- </button>`;
1555
- } else {
1556
- absDisplay +=
1557
- `<div class="mt-1"><button onclick="toggleLogAbsentee(${s.id})" class="inline-flex items-center gap-1 text-[11px] font-bold text-slate-600 bg-slate-100 hover:bg-slate-200 border border-slate-300 px-2 py-0.5 rounded-lg transition shadow-2xs">
1558
- Collapse <i data-lucide="chevron-up" class="w-3 h-3"></i>
1559
- </button></div>`;
1560
- }
1561
- }
1562
- }
1563
-
1564
- html += `
1565
- <tr class="hover:bg-slate-50 transition-colors">
1566
- <td class="py-3.5 px-3 text-center">
1567
- <input type="checkbox" data-id="${s.id}" ${isChecked ? 'checked' : ''} onchange="toggleLogSelection(${s.id}, this)" class="log-item-checkbox rounded border-slate-300 text-emerald-600 focus:ring-emerald-500">
1568
- </td>
1569
- <td class="py-3.5 px-4 whitespace-nowrap font-medium text-slate-800">${s.date} <span class="text-slate-400 font-normal ml-1 text-xs">(${s.dayKey})</span></td>
1570
- <td class="py-3.5 px-4 font-bold text-emerald-600">${s.class}</td>
1571
- <td class="py-3.5 px-4 font-semibold text-slate-700">${s.subject}</td>
1572
- <td class="py-3.5 px-4 truncate max-w-[160px] text-slate-600 uppercase text-xs font-medium tracking-wide" title="${s.teacher}">${s.teacher}</td>
1573
- <td class="py-3.5 px-4 whitespace-nowrap text-slate-500">${s.startTime} - ${s.endTime}</td>
1574
- <td class="py-3.5 px-4 text-center font-bold text-slate-800">${s.present} / ${s.total}</td>
1575
- <td class="py-3.5 px-4 text-center text-rose-600 font-bold">${s.absent}</td>
1576
- <td class="py-3.5 px-4 min-w-[220px] max-w-[320px] leading-tight pt-3.5">${absDisplay}</td>
1577
- <td class="py-3.5 px-4 text-center font-bold text-sky-600">${s.rate}</td>
1578
- <td class="py-3.5 px-4 text-right whitespace-nowrap">
1579
- <button type="button" data-action="delete-session" data-id="${s.id}" class="js-delete-session bg-rose-50 hover:bg-rose-600 text-rose-700 hover:text-white border border-rose-200 hover:border-rose-600 px-2.5 py-1.5 rounded-lg text-[11px] font-bold transition inline-flex items-center gap-1 cursor-pointer" title="Delete this log">
1580
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
1581
- <span class="pointer-events-none">Delete</span>
1582
- </button>
1583
- </td>
1584
- </tr>
1585
- `;
1586
- });
1587
-
1588
- if (sorted.length === 0) {
1589
- html =
1590
- `<tr><td colspan="11" class="text-center py-8 text-slate-500 text-sm">No attendance sessions found. Create a log or restore CSV logs to get started.</td></tr>`;
1591
- }
1592
- tbody.innerHTML = html;
1593
- updateDeleteSelectedButton();
1594
- updateRestoredDatesDropdown();
1595
- lucide.createIcons();
1596
- }
1597
- window.renderHistoryTable = renderHistoryTable;
1598
-
1599
- // ========== DELETE SYSTEM (Canvas-safe: no confirm/alert, event delegation) ==========
1600
- window._pendingDeleteId = null;
1601
- window._pendingDeleteMode = null; // 'single' | 'bulk'
1602
-
1603
- function openDeleteConfirmModal(mode, id) {
1604
- window._pendingDeleteMode = mode;
1605
- window._pendingDeleteId = id != null ? id : null;
1606
- const titleEl = document.getElementById('delete-confirm-title');
1607
- const bodyEl = document.getElementById('delete-confirm-body');
1608
- if (mode === 'bulk') {
1609
- const count = window.selectedLogIds.size;
1610
- if (titleEl) titleEl.textContent = 'Delete selected logs?';
1611
- if (bodyEl) bodyEl.textContent = 'Are you sure you want to delete ' + count + ' selected session log(s)?';
1612
- } else {
1613
- if (titleEl) titleEl.textContent = 'Delete this log?';
1614
- if (bodyEl) bodyEl.textContent = 'Are you sure you want to permanently delete this attendance session?';
1615
- }
1616
- const modal = document.getElementById('delete-confirm-modal-backdrop');
1617
- if (modal) {
1618
- modal.classList.remove('hidden');
1619
- try { lucide.createIcons(); } catch (e) {}
1620
- }
1621
- }
1622
-
1623
- function closeDeleteConfirmModal() {
1624
- const modal = document.getElementById('delete-confirm-modal-backdrop');
1625
- if (modal) modal.classList.add('hidden');
1626
- window._pendingDeleteId = null;
1627
- window._pendingDeleteMode = null;
1628
- }
1629
-
1630
- function executePendingDelete() {
1631
- const mode = window._pendingDeleteMode;
1632
- const id = window._pendingDeleteId;
1633
- closeDeleteConfirmModal();
1634
- if (mode === 'bulk') {
1635
- performBulkDelete();
1636
- } else if (id != null) {
1637
- performDeleteSession(id);
1638
- }
1639
- }
1640
-
1641
- function refreshAllDataViews() {
1642
- try { renderHistoryTable(); } catch (e) {}
1643
- try { renderTimetableGrid(); } catch (e) {}
1644
- try { renderAnalyticsDashboard(); } catch (e) {}
1645
- }
1646
- window.refreshAllDataViews = refreshAllDataViews;
1647
-
1648
- function performDeleteSession(id) {
1649
- const session = window.savedSessions.find(s => String(s.id) === String(id));
1650
- if (!session) {
1651
- showToast("Error", "Session not found.");
1652
- return;
1653
- }
1654
- window.savedSessions = window.savedSessions.filter(s => String(s.id) !== String(id));
1655
- try {
1656
- window.selectedLogIds.delete(Number(id));
1657
- window.selectedLogIds.delete(id);
1658
- window.selectedLogIds.delete(String(id));
1659
- } catch (e) {}
1660
- window.safeSetItem('smk_saved_sessions_v1', JSON.stringify(window.savedSessions));
1661
- if (typeof window.deleteSessionFromCloud === 'function') {
1662
- try { window.deleteSessionFromCloud(session); } catch (e) {}
1663
- }
1664
- refreshAllDataViews();
1665
- showToast("Session Deleted", "Record removed. Analytics updated.");
1666
- }
1667
-
1668
- // Keep old name for compatibility — direct delete (no confirm popup)
1669
- function deleteSession(id) {
1670
- performDeleteSession(id);
1671
- }
1672
- window.deleteSession = deleteSession;
1673
- window.performDeleteSession = performDeleteSession;
1674
-
1675
- function performBulkDelete() {
1676
- const count = window.selectedLogIds.size;
1677
- if (count === 0) return;
1678
- const idsToDelete = Array.from(window.selectedLogIds);
1679
- idsToDelete.forEach(id => {
1680
- const session = window.savedSessions.find(s => String(s.id) === String(id));
1681
- if (session && typeof window.deleteSessionFromCloud === 'function') {
1682
- try { window.deleteSessionFromCloud(session); } catch (e) {}
1683
- }
1684
- });
1685
- window.savedSessions = window.savedSessions.filter(s => {
1686
- return !window.selectedLogIds.has(s.id) &&
1687
- !window.selectedLogIds.has(Number(s.id)) &&
1688
- !window.selectedLogIds.has(String(s.id));
1689
- });
1690
- window.selectedLogIds.clear();
1691
- window.safeSetItem('smk_saved_sessions_v1', JSON.stringify(window.savedSessions));
1692
- const masterCb = document.getElementById('select-all-logs');
1693
- if (masterCb) masterCb.checked = false;
1694
- refreshAllDataViews();
1695
- showToast("Bulk Deletion Complete", count + " logs deleted. Analytics updated.");
1696
- }
1697
-
1698
- function deleteSelectedSessions() {
1699
- if (window.selectedLogIds.size === 0) return;
1700
- openDeleteConfirmModal('bulk', null);
1701
- }
1702
- window.deleteSelectedSessions = deleteSelectedSessions;
1703
- window.openDeleteConfirmModal = openDeleteConfirmModal;
1704
- window.closeDeleteConfirmModal = closeDeleteConfirmModal;
1705
- window.executePendingDelete = executePendingDelete;
1706
- window.performBulkDelete = performBulkDelete;
1707
-
1708
- function confirmClearAllLogs() {
1709
- if (window.savedSessions.length === 0) {
1710
- showToast("No Logs", "History log is already empty.");
1711
- return;
1712
- }
1713
- document.getElementById('clear-logs-modal-backdrop').classList.remove('hidden');
1714
- }
1715
- window.confirmClearAllLogs = confirmClearAllLogs;
1716
-
1717
- function closeClearLogsModal() {
1718
- document.getElementById('clear-logs-modal-backdrop').classList.add('hidden');
1719
- }
1720
- window.closeClearLogsModal = closeClearLogsModal;
1721
-
1722
- function executeClearAllLogs() {
1723
- window.savedSessions.forEach(session => {
1724
- if (typeof window.deleteSessionFromCloud === 'function') {
1725
- try { window.deleteSessionFromCloud(session); } catch (e) {}
1726
- }
1727
- });
1728
-
1729
- window.savedSessions = [];
1730
- window.selectedLogIds.clear();
1731
- window.safeSetItem('smk_saved_sessions_v1', JSON.stringify([]));
1732
-
1733
- closeClearLogsModal();
1734
- refreshAllDataViews();
1735
- showToast("Cleared All Logs", "All logs cleared. Analytics updated.");
1736
- }
1737
- window.executeClearAllLogs = executeClearAllLogs;
1738
-
1739
- // Timetable Viewer Logic with Restored Logs Mapping
1740
- function getWeekBounds(dateStr) {
1741
- const d = parseDateString(dateStr) || new Date();
1742
- const day = d.getDay();
1743
- const diffToMon = day === 0 ? -6 : 1 - day;
1744
-
1745
- const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate() + diffToMon);
1746
- const sunday = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + 6);
1747
-
1748
- return {
1749
- start: formatDateToISO(monday),
1750
- end: formatDateToISO(sunday)
1751
- };
1752
- }
1753
-
1754
- function updateRestoredDatesDropdown() {
1755
- const wrapper = document.getElementById('timetable-date-dropdown-wrapper');
1756
- const select = document.getElementById('timetable-restored-dates');
1757
- if (!wrapper || !select) return;
1758
-
1759
- const dates = Array.from(new Set(window.savedSessions.map(s => s.date))).filter(Boolean).sort().reverse();
1760
- if (dates.length > 0) {
1761
- select.innerHTML = `<option value="">-- Jump to Log Date --</option>` +
1762
- dates.map(d => `<option value="${d}">Log Date: ${d}</option>`).join('');
1763
- wrapper.classList.remove('hidden');
1764
- } else {
1765
- wrapper.classList.add('hidden');
1766
- }
1767
- }
1768
-
1769
- function jumpToRestoredDate(dateVal) {
1770
- if (!dateVal) return;
1771
- const ttInput = document.getElementById('timetable-date');
1772
- if (ttInput) {
1773
- ttInput.value = dateVal;
1774
- renderTimetableGrid();
1775
- showToast("Timetable Jumped", `Viewing timetable for week of ${dateVal}`);
1776
- }
1777
- }
1778
-
1779
- function renderTimetableGrid() {
1780
- const className = document.getElementById('timetable-view-class').value;
1781
- const refDate = document.getElementById('timetable-date').value || formatDateToISO(new Date());
1782
- const bounds = getWeekBounds(refDate);
1783
-
1784
- document.getElementById('tt-class-title').innerText = className;
1785
- document.getElementById('tt-class-teacher').innerHTML =
1786
- `Class Attendance Grid <br><span class="text-[10.5px] font-bold text-emerald-200 tracking-wider">WEEK: ${bounds.start} to ${bounds.end}</span>`;
1787
-
1788
- const theadTr = document.querySelector('#tab-timetable thead tr');
1789
- if (theadTr) {
1790
- let ths = '';
1791
- window.TIME_SLOTS.forEach((slot, i) => {
1792
- ths +=
1793
- `<th class="p-2 border-r border-slate-800 min-w-[65px]"><span class="text-xs font-extrabold text-slate-100">${i+1}</span><br><span class="text-[9.5px] font-medium text-slate-400">${slot}</span></th>`;
1794
- });
1795
- theadTr.innerHTML =
1796
- `<th class="p-2.5 border-r border-slate-800 font-bold w-16 sticky left-0 bg-slate-950 z-10 text-white">Day</th>` +
1797
- ths;
1798
- }
1799
-
1800
- // Filter sessions for selected class matching week bounds OR date parsing
1801
- const classSessions = window.savedSessions.filter(s => {
1802
- if (s.class !== className) return false;
1803
- const sIso = formatDateToISO(parseDateString(s.date));
1804
- return sIso >= bounds.start && sIso <= bounds.end;
1805
- });
1806
-
1807
- document.getElementById('tt-stats-badge').innerText = `${classSessions.length} recorded session(s) this week`;
1808
-
1809
- const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
1810
- const tbody = document.getElementById('timetable-grid-body');
1811
- let html = '';
1812
-
1813
- days.forEach(day => {
1814
- html +=
1815
- `<tr class="hover:bg-slate-50/50"><td class="p-2.5 border-r border-slate-200 font-bold bg-slate-100 uppercase text-[11px] tracking-wide text-slate-700 sticky left-0 z-10">${day}</td>`;
1816
-
1817
- window.TIME_SLOTS.forEach(slot => {
1818
- const sess = classSessions.find(s => {
1819
- const parsedD = parseDateString(s.date);
1820
- const sDay = getDayKeyFromDate(parsedD).toUpperCase().slice(0, 3);
1821
- const matchDay = sDay === day.toUpperCase();
1822
- if (!matchDay) return false;
1823
-
1824
- if (s.startTime === slot) return true;
1825
- if (s.startTime && s.endTime) {
1826
- return slot >= s.startTime && slot < s.endTime;
1827
- }
1828
- return false;
1829
- });
1830
-
1831
- if (sess) {
1832
- const abbr = getSubjectAbbr(sess.subject);
1833
- const displayStat = `${sess.present}/${sess.total}`;
1834
-
1835
- html += `
1836
- <td class="p-1 border-r border-slate-200 align-middle group relative">
1837
- <div class="bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg p-1.5 h-[52px] flex flex-col items-center justify-center gap-0.5 shadow-sm transition-colors relative cursor-default">
1838
- <span class="text-[11px] font-extrabold tracking-wide uppercase leading-none">${abbr}</span>
1839
- <span class="text-[10px] font-bold text-emerald-100 flex items-center gap-0.5 leading-none mt-0.5">
1840
- ${displayStat}
1841
- </span>
1842
- <button type="button" data-action="delete-session" data-id="${sess.id}" class="js-delete-session absolute -top-1.5 -right-1.5 bg-rose-600 text-white rounded-full w-4 h-4 text-[10px] flex items-center justify-center shadow-md hover:bg-rose-700 z-10 cursor-pointer" title="Remove Session">×</button>
1843
- </div>
1844
- </td>`;
1845
- } else {
1846
- html +=
1847
- `<td class="p-1 border-r border-slate-200"><div class="h-[52px] rounded-lg border border-dashed border-slate-200/80 bg-slate-50/30 flex items-center justify-center text-slate-300 text-xs font-semibold">-</div></td>`;
1848
- }
1849
- });
1850
- html += `</tr>`;
1851
- });
1852
-
1853
- tbody.innerHTML = html;
1854
- lucide.createIcons();
1855
- }
1856
-
1857
- function onRosterManagerClassChange() {
1858
- const className = document.getElementById('roster-manager-class-select').value;
1859
- document.getElementById('rm-class-name').innerText = className;
1860
-
1861
- const students = window.rosters[className] || [];
1862
- document.getElementById('rm-total-students').innerText = `${students.length} Pax`;
1863
-
1864
- const tbody = document.getElementById('roster-manager-table-body');
1865
- let html = '';
1866
- if (students.length === 0) {
1867
- html =
1868
- `<tr><td colspan="2" class="text-center py-4 text-slate-500 text-xs">No specific roster found. System will generate default placeholders.</td></tr>`;
1869
- } else {
1870
- students.forEach((s, i) => {
1871
- html +=
1872
- `<tr class="hover:bg-slate-50"><td class="py-2.5 px-4 text-center font-medium text-slate-500">${i+1}</td><td class="py-2.5 px-4 font-bold text-slate-800">${s}</td></tr>`;
1873
- });
1874
- }
1875
- tbody.innerHTML = html;
1876
- }
1877
-
1878
- function openRosterModal(className) {
1879
- window.activeModalClass = className || document.getElementById('select-class').value;
1880
- document.getElementById('modal-roster-class-name').innerText = window.activeModalClass;
1881
-
1882
- const students = window.rosters[window.activeModalClass] || [];
1883
- document.getElementById('input-roster-textarea').value = students.join('\n');
1884
-
1885
- document.getElementById('roster-modal-backdrop').classList.remove('hidden');
1886
- }
1887
-
1888
- function closeRosterModal() {
1889
- document.getElementById('roster-modal-backdrop').classList.add('hidden');
1890
- }
1891
-
1892
- function clearCustomRoster() {
1893
- document.getElementById('input-roster-textarea').value = "";
1894
- showToast("Roster Cleared", "Student list cleared (click Save Roster to apply).");
1895
- }
1896
- window.clearCustomRoster = clearCustomRoster;
1897
-
1898
- function saveRosterModal() {
1899
- const text = document.getElementById('input-roster-textarea').value;
1900
- const list = text.split('\n').map(s => s.trim().toUpperCase()).filter(s => s.length > 0);
1901
-
1902
- window.rosters[window.activeModalClass] = list;
1903
- window.safeSetItem('smk_custom_rosters_v3', JSON.stringify(window.rosters));
1904
-
1905
- if (typeof window.syncRostersToCloud === 'function') {
1906
- window.syncRostersToCloud(window.rosters, window.ALL_CLASSES, window.MASTER_TEACHER_LIST, window
1907
- .SUBJECT_LIST);
1908
- }
1909
-
1910
- closeRosterModal();
1911
- showToast("Roster Updated", `${list.length} students saved for class ${window.activeModalClass}`);
1912
- refreshAllUI();
1913
- }
1914
-
1915
- window.activeListType = "";
1916
-
1917
- function openListModal(type) {
1918
- window.activeListType = type;
1919
- document.getElementById('modal-list-title').innerText = type === 'subjects' ? 'Subjects' : 'Teachers';
1920
- const list = type === 'subjects' ? window.SUBJECT_LIST : window.MASTER_TEACHER_LIST;
1921
- document.getElementById('input-list-textarea').value = list.join('\n');
1922
-
1923
- document.getElementById('list-edit-modal-backdrop').classList.remove('hidden');
1924
- }
1925
-
1926
- function closeListModal() {
1927
- document.getElementById('list-edit-modal-backdrop').classList.add('hidden');
1928
- }
1929
-
1930
- function clearList() {
1931
- document.getElementById('input-list-textarea').value = "";
1932
- showToast("List Cleared", "List cleared (click Save List to apply).");
1933
- }
1934
- window.clearList = clearList;
1935
-
1936
- function saveListModal() {
1937
- const text = document.getElementById('input-list-textarea').value;
1938
- const list = text.split('\n').map(s => s.trim()).filter(s => s.length > 0);
1939
-
1940
- if (window.activeListType === 'subjects') {
1941
- window.SUBJECT_LIST = list;
1942
- window.safeSetItem('smk_subjects_v1', JSON.stringify(list));
1943
- } else {
1944
- window.MASTER_TEACHER_LIST = list;
1945
- window.safeSetItem('smk_teachers_v1', JSON.stringify(list));
1946
- }
1947
-
1948
- if (typeof window.syncRostersToCloud === 'function') {
1949
- window.syncRostersToCloud(window.rosters, window.ALL_CLASSES, window.MASTER_TEACHER_LIST, window
1950
- .SUBJECT_LIST);
1951
- }
1952
-
1953
- closeListModal();
1954
- showToast("List Updated", `Saved ${list.length} items successfully.`);
1955
- refreshAllUI();
1956
- }
1957
-
1958
- function refreshAllUI() {
1959
- populateDropdowns();
1960
- onClassChange();
1961
- onRosterManagerClassChange();
1962
- renderTimetableGrid();
1963
- try { renderAnalyticsDashboard(); } catch (e) {}
1964
- }
1965
- window.refreshAllUI = refreshAllUI;
1966
- window.renderAnalyticsDashboard = renderAnalyticsDashboard;
1967
-
1968
- window.classAttendanceChartInstance = null;
1969
- window.submissionStatusChartInstance = null;
1970
-
1971
- function renderAnalyticsDashboard() {
1972
- const selectedDate = document.getElementById('analytics-date-input').value || formatDateToISO(new Date());
1973
- const threshold = parseFloat(document.getElementById('analytics-threshold-select').value) || 85;
1974
-
1975
- const targetSessions = window.savedSessions.filter(s => formatDateToISO(parseDateString(s.date)) === selectedDate);
1976
-
1977
- const loggedClasses = new Set(targetSessions.map(s => s.class));
1978
- const pendingClasses = window.ALL_CLASSES.filter(c => !loggedClasses.has(c));
1979
-
1980
- const totalLoggedCount = targetSessions.length;
1981
- const pendingCount = pendingClasses.length;
1982
-
1983
- let totalPresentAll = 0;
1984
- let totalEnrolledAll = 0;
1985
- let underperformingSessions = [];
1986
-
1987
- const classRateMap = {};
1988
- window.ALL_CLASSES.forEach(c => {
1989
- const cSessions = targetSessions.filter(s => s.class === c);
1990
- if (cSessions.length > 0) {
1991
- let cPresent = 0,
1992
- cTotal = 0;
1993
- cSessions.forEach(s => {
1994
- cPresent += s.present;
1995
- cTotal += s.total;
1996
-
1997
- const numericRate = parseFloat(s.rate.replace('%', '')) || 0;
1998
- if (numericRate < threshold) {
1999
- underperformingSessions.push(s);
2000
- }
2001
- });
2002
- totalPresentAll += cPresent;
2003
- totalEnrolledAll += cTotal;
2004
- classRateMap[c] = cTotal > 0 ? parseFloat(((cPresent / cTotal) * 100).toFixed(1)) : 0;
2005
- } else {
2006
- classRateMap[c] = null;
2007
- }
2008
- });
2009
-
2010
- const overallSchoolRate = totalEnrolledAll > 0 ? ((totalPresentAll / totalEnrolledAll) * 100).toFixed(1) : 0;
2011
-
2012
- document.getElementById('kpi-logged-sessions').innerText = totalLoggedCount;
2013
- document.getElementById('kpi-pending-classes').innerText = `${pendingCount} Classes`;
2014
- document.getElementById('kpi-school-rate').innerText = `${overallSchoolRate}%`;
2015
- document.getElementById('kpi-underperforming-count').innerText = `${underperformingSessions.length} Alert(s)`;
2016
-
2017
- document.getElementById('pending-table-count').innerText = pendingCount;
2018
- document.getElementById('underperforming-table-count').innerText = underperformingSessions.length;
2019
-
2020
- const pendingBody = document.getElementById('table-pending-keyin-body');
2021
- if (pendingClasses.length === 0) {
2022
- pendingBody.innerHTML =
2023
- `<tr><td colspan="3" class="text-center py-6 text-emerald-600 font-bold">✓ All ${window.ALL_CLASSES.length} classes have keyed in attendance for ${selectedDate}!</td></tr>`;
2024
- } else {
2025
- pendingBody.innerHTML = pendingClasses.map(c => `
2026
- <tr class="hover:bg-amber-50/50">
2027
- <td class="py-2.5 px-3 font-bold text-amber-900 flex items-center gap-2">
2028
- <span class="w-2 h-2 rounded-full bg-amber-500"></span> ${c}
2029
- </td>
2030
- <td class="py-2.5 px-3 text-center">
2031
- <span class="bg-amber-100 text-amber-800 text-[10px] font-bold px-2 py-0.5 rounded border border-amber-200">Pending Key-In</span>
2032
- </td>
2033
- <td class="py-2.5 px-3 text-right">
2034
- <button onclick="quickSelectClassForAttendance('${c}')" class="text-[11px] font-bold text-emerald-700 bg-emerald-50 hover:bg-emerald-100 px-2 py-1 rounded border border-emerald-200 transition">
2035
- Key In Now
2036
- </button>
2037
- </td>
2038
- </tr>
2039
- `).join('');
2040
- }
2041
-
2042
- const underperformingBody = document.getElementById('table-underperforming-body');
2043
- if (underperformingSessions.length === 0) {
2044
- underperformingBody.innerHTML =
2045
- `<tr><td colspan="4" class="text-center py-6 text-emerald-600 font-bold">✓ No lessons found below ${threshold}% attendance threshold.</td></tr>`;
2046
- } else {
2047
- underperformingBody.innerHTML = underperformingSessions.map(s => `
2048
- <tr class="hover:bg-rose-50/50">
2049
- <td class="py-2.5 px-3">
2050
- <span class="font-bold text-rose-800">${s.class}</span> - <span class="text-slate-700 font-semibold">${s.subject}</span>
2051
- <br><span class="text-[10px] text-slate-400">${s.startTime} - ${s.endTime}</span>
2052
- </td>
2053
- <td class="py-2.5 px-3 text-slate-600 font-medium text-[11px]">${s.teacher}</td>
2054
- <td class="py-2.5 px-3 text-center font-bold text-slate-800">${s.present} / ${s.total}</td>
2055
- <td class="py-2.5 px-3 text-center">
2056
- <span class="bg-rose-100 text-rose-800 text-[11px] font-extrabold px-2 py-0.5 rounded border border-rose-200">${s.rate}</span>
2057
- </td>
2058
- </tr>
2059
- `).join('');
2060
- }
2061
-
2062
- renderClassAttendanceChart(classRateMap, threshold);
2063
- renderSubmissionStatusChart(loggedClasses.size, pendingCount);
2064
-
2065
- lucide.createIcons();
2066
- }
2067
-
2068
- function quickSelectClassForAttendance(className) {
2069
- document.getElementById('select-class').value = className;
2070
- switchTab('tracker');
2071
- }
2072
-
2073
- function renderClassAttendanceChart(classRateMap, threshold) {
2074
- const ctx = document.getElementById('chart-class-attendance')?.getContext('2d');
2075
- if (!ctx) return;
2076
-
2077
- const labels = window.ALL_CLASSES;
2078
- const rates = labels.map(c => classRateMap[c] !== null ? classRateMap[c] : 0);
2079
- const backgroundColors = labels.map(c => {
2080
- const val = classRateMap[c];
2081
- if (val === null) return 'rgba(203, 213, 225, 0.4)';
2082
- return val < threshold ? 'rgba(225, 29, 72, 0.85)' : 'rgba(16, 185, 129, 0.85)';
2083
- });
2084
-
2085
- if (window.classAttendanceChartInstance) {
2086
- window.classAttendanceChartInstance.destroy();
2087
- }
2088
-
2089
- window.classAttendanceChartInstance = new Chart(ctx, {
2090
- type: 'bar',
2091
- data: {
2092
- labels: labels,
2093
- datasets: [{
2094
- label: 'Attendance Rate (%)',
2095
- data: rates,
2096
- backgroundColor: backgroundColors,
2097
- borderRadius: 6,
2098
- maxBarThickness: 28
2099
- }]
2100
- },
2101
- options: {
2102
- responsive: true,
2103
- maintainAspectRatio: false,
2104
- plugins: {
2105
- legend: { display: false },
2106
- tooltip: {
2107
- callbacks: {
2108
- label: function(context) {
2109
- const cls = context.label;
2110
- const rate = classRateMap[cls];
2111
- if (rate === null) return `${cls}: Yet to key in`;
2112
- return `${cls}: ${rate}% Attendance`;
2113
- }
2114
- }
2115
- }
2116
- },
2117
- scales: {
2118
- y: {
2119
- beginAtZero: true,
2120
- max: 100,
2121
- ticks: {
2122
- callback: value => value + '%'
2123
- }
2124
- },
2125
- x: {
2126
- grid: { display: false }
2127
- }
2128
- }
2129
- }
2130
- });
2131
- }
2132
-
2133
- function renderSubmissionStatusChart(loggedCount, pendingCount) {
2134
- const ctx = document.getElementById('chart-submission-status')?.getContext('2d');
2135
- if (!ctx) return;
2136
-
2137
- if (window.submissionStatusChartInstance) {
2138
- window.submissionStatusChartInstance.destroy();
2139
- }
2140
-
2141
- window.submissionStatusChartInstance = new Chart(ctx, {
2142
- type: 'doughnut',
2143
- data: {
2144
- labels: ['Attendance Keyed In', 'Yet to Key In'],
2145
- datasets: [{
2146
- data: [loggedCount, pendingCount],
2147
- backgroundColor: ['rgba(16, 185, 129, 0.85)', 'rgba(245, 158, 11, 0.85)'],
2148
- borderWidth: 2,
2149
- borderColor: '#ffffff'
2150
- }]
2151
- },
2152
- options: {
2153
- responsive: true,
2154
- maintainAspectRatio: false,
2155
- plugins: {
2156
- legend: {
2157
- position: 'bottom',
2158
- labels: {
2159
- font: { size: 11, weight: 'bold' }
2160
- }
2161
- }
2162
- },
2163
- cutout: '68%'
2164
- }
2165
- });
2166
- }
2167
-
2168
- function showToast(title, msg) {
2169
- const titleEl = document.getElementById('toast-title');
2170
- const bodyEl = document.getElementById('toast-body');
2171
- const t = document.getElementById('toast-msg');
2172
- if (titleEl) titleEl.innerText = title;
2173
- if (bodyEl) bodyEl.innerText = msg;
2174
- if (t) {
2175
- t.classList.remove('opacity-0', 'translate-y-20', 'pointer-events-none');
2176
- setTimeout(() => {
2177
- t.classList.add('opacity-0', 'translate-y-20', 'pointer-events-none');
2178
- }, 3000);
2179
- }
2180
- }
2181
- window.showToast = showToast;
2182
-
2183
- function copySummaryToClipboard() {
2184
- const className = document.getElementById('select-class').value;
2185
- const subject = document.getElementById('select-subject').value;
2186
- const total = parseInt(document.getElementById('input-total-enrolled').value) || 0;
2187
- const present = parseInt(document.getElementById('input-present-count').value) || 0;
2188
- const absent = total - present;
2189
-
2190
- let absentList = [];
2191
- for (let i = 0; i < absent; i++) {
2192
- const selectEl = document.getElementById(`absentee-select-${i}`);
2193
- const nameEl = document.getElementById(`absentee-name-${i}`);
2194
- const reasonEl = document.getElementById(`absentee-reason-${i}`);
2195
- const remarkEl = document.getElementById(`absentee-remark-${i}`);
2196
-
2197
- let sName = nameEl && nameEl.value.trim() ? nameEl.value.trim() : (selectEl && selectEl.value ? selectEl.value :
2198
- "Unknown");
2199
- let sReason = reasonEl ? reasonEl.value : "No Reason";
2200
- if (remarkEl && remarkEl.value.trim()) {
2201
- sReason += ` (${remarkEl.value.trim()})`;
2202
- }
2203
- absentList.push({ name: sName, reason: sReason });
2204
- }
2205
-
2206
- let txt = `*ATTENDANCE SUMMARY*\nClass: ${className}\nSubject: ${subject}\nPresent: ${present}/${total}\n`;
2207
- if (absentList.length > 0) {
2208
- txt += `\n*Absentees:*\n`;
2209
- absentList.forEach((a, i) => {
2210
- txt += `${i+1}. ${a.name} - ${a.reason}\n`;
2211
- });
2212
- } else {
2213
- txt += `\n*All students present!*`;
2214
- }
2215
-
2216
- const el = document.createElement('textarea');
2217
- el.value = txt;
2218
- document.body.appendChild(el);
2219
- el.select();
2220
- document.execCommand('copy');
2221
- document.body.removeChild(el);
2222
- showToast("Copied", "Summary copied to clipboard");
2223
- }
2224
-
2225
- function escapeCSV(str) {
2226
- if (str === null || str === undefined) return '""';
2227
- const s = String(str);
2228
- if (s.includes('"') || s.includes(',')) {
2229
- return `"${s.replace(/"/g, '""')}"`;
2230
- }
2231
- return s;
2232
- }
2233
-
2234
- function parseCSVLine(text) {
2235
- const result = [];
2236
- let cur = '';
2237
- let inQuotes = false;
2238
- for (let i = 0; i < text.length; i++) {
2239
- const char = text[i];
2240
- if (char === '"' && text[i + 1] === '"') {
2241
- cur += '"';
2242
- i++;
2243
- } else if (char === '"') {
2244
- inQuotes = !inQuotes;
2245
- } else if (char === ',' && !inQuotes) {
2246
- result.push(cur);
2247
- cur = '';
2248
- } else {
2249
- cur += char;
2250
- }
2251
- }
2252
- result.push(cur);
2253
- return result;
2254
- }
2255
-
2256
- function exportAllCSV() {
2257
- if (window.savedSessions.length === 0) {
2258
- alert("No data to export.");
2259
- return;
2260
- }
2261
- const headers = ["Date", "Class", "Subject", "Teacher", "Slot", "Total", "Present", "Absent", "Absentees", "Rate"];
2262
- let csvContent = headers.join(",") + "\n";
2263
-
2264
- const sorted = [...window.savedSessions].sort((a, b) => b.id - a.id);
2265
- sorted.forEach(s => {
2266
- const absenteesStr = s.absentees ? s.absentees.map(ab => `${ab.name} (${ab.reason})`).join(" | ") : "";
2267
- const row = [
2268
- s.date, s.class, s.subject, s.teacher, `${s.startTime} - ${s.endTime}`,
2269
- s.total, s.present, s.absent, absenteesStr, s.rate
2270
- ];
2271
- csvContent += row.map(escapeCSV).join(",") + "\n";
2272
- });
2273
-
2274
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
2275
- const link = document.createElement("a");
2276
- const url = URL.createObjectURL(blob);
2277
- link.setAttribute("href", url);
2278
- link.setAttribute("download", `SMK_Attendance_Logs_${formatDateToISO(new Date())}.csv`);
2279
- link.style.visibility = 'hidden';
2280
- document.body.appendChild(link);
2281
- link.click();
2282
- document.body.removeChild(link);
2283
- }
2284
-
2285
- function handleLogCSVUpload(event) {
2286
- const file = event.target.files[0];
2287
- if (!file) return;
2288
-
2289
- const reader = new FileReader();
2290
- reader.onload = async function(e) {
2291
- const text = e.target.result;
2292
- const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0);
2293
-
2294
- if (lines.length < 2) {
2295
- alert("Invalid CSV or empty file.");
2296
- return;
2297
- }
2298
-
2299
- const headerLine = lines[0].toLowerCase();
2300
- if (!headerLine.includes('date') || !headerLine.includes('class') || !headerLine.includes('subject')) {
2301
- alert("CSV format unrecognized. Expected headers containing: Date, Class, Subject...");
2302
- return;
2303
- }
2304
-
2305
- let importedCount = 0;
2306
- let firstImportedDate = null;
2307
-
2308
- for (let i = 1; i < lines.length; i++) {
2309
- try {
2310
- const cols = parseCSVLine(lines[i]);
2311
- if (cols.length < 8) continue;
2312
-
2313
- const rawDateStr = cols[0];
2314
- const dateObj = parseDateString(rawDateStr) || new Date();
2315
- const isoDate = formatDateToISO(dateObj);
2316
- if (!firstImportedDate) firstImportedDate = isoDate;
2317
-
2318
- const className = cols[1];
2319
- const subject = cols[2];
2320
- const teacher = cols[3];
2321
-
2322
- let startTime = "07:00",
2323
- endTime = "07:30";
2324
- let totalIdx = 5,
2325
- presentIdx = 6,
2326
- absentIdx = 7,
2327
- absenteesIdx = 8,
2328
- rateIdx = 9;
2329
-
2330
- if (cols[4] && cols[4].includes('-')) {
2331
- const pts = cols[4].split('-');
2332
- startTime = pts[0].trim();
2333
- endTime = pts[1].trim();
2334
- } else if (cols[4] && cols[5] && cols[4].includes(':') && cols[5].includes(':')) {
2335
- startTime = cols[4].trim();
2336
- endTime = cols[5].trim();
2337
- totalIdx = 6;
2338
- presentIdx = 7;
2339
- absentIdx = 8;
2340
- rateIdx = 9;
2341
- absenteesIdx = 10;
2342
- }
2343
-
2344
- const total = parseInt(cols[totalIdx]) || 0;
2345
- const present = parseInt(cols[presentIdx]) || 0;
2346
- const absent = parseInt(cols[absentIdx]) || 0;
2347
- const rate = cols[rateIdx] || (total > 0 ? ((present / total) * 100).toFixed(1) + '%' : '0%');
2348
- const absenteesStr = cols[absenteesIdx] || "";
2349
-
2350
- const dayKey = getDayKeyFromDate(dateObj);
2351
-
2352
- const absentees = [];
2353
- if (absenteesStr && absenteesStr !== "None" && absenteesStr.trim() !== "") {
2354
- const delimiter = absenteesStr.includes('|') ? '|' : ',';
2355
- const absParts = absenteesStr.split(delimiter);
2356
- absParts.forEach(part => {
2357
- part = part.trim();
2358
- if (!part) return;
2359
- const match = part.match(/(.*?)[\(\[](.*?)[\)\]]/);
2360
- if (match) {
2361
- absentees.push({ name: match[1].trim(), reason: match[2].trim() });
2362
- } else {
2363
- absentees.push({ name: part, reason: "N/A" });
2364
- }
2365
- });
2366
- }
2367
-
2368
- const sessionRecord = {
2369
- id: Date.now() + i,
2370
- date: isoDate,
2371
- dayKey,
2372
- class: className,
2373
- subject,
2374
- teacher,
2375
- startTime,
2376
- endTime,
2377
- total,
2378
- present,
2379
- absent,
2380
- rate,
2381
- absentees
2382
- };
2383
-
2384
- if (teacher && !window.MASTER_TEACHER_LIST.includes(teacher)) {
2385
- window.MASTER_TEACHER_LIST.push(teacher);
2386
- }
2387
- if (subject && !window.SUBJECT_LIST.includes(subject)) {
2388
- window.SUBJECT_LIST.push(subject);
2389
- }
2390
- if (className && !window.ALL_CLASSES.includes(className)) {
2391
- window.ALL_CLASSES.push(className);
2392
- window.ALL_CLASSES.sort();
2393
- }
2394
-
2395
- window.savedSessions = [sessionRecord, ...window.savedSessions.filter(s => s.id !== sessionRecord
2396
- .id)];
2397
-
2398
- if (typeof window.saveSessionToCloud === 'function') {
2399
- await window.saveSessionToCloud(sessionRecord);
2400
- }
2401
-
2402
- importedCount++;
2403
-
2404
- } catch (err) {
2405
- console.warn("Failed to parse row: ", lines[i], err);
2406
- }
2407
- }
2408
-
2409
- if (typeof window.syncRostersToCloud === 'function') {
2410
- window.syncRostersToCloud(window.rosters, window.ALL_CLASSES, window.MASTER_TEACHER_LIST, window
2411
- .SUBJECT_LIST);
2412
- }
2413
-
2414
- window.safeSetItem('smk_saved_sessions_v1', JSON.stringify(window.savedSessions));
2415
- window.safeSetItem('smk_teachers_v1', JSON.stringify(window.MASTER_TEACHER_LIST));
2416
- window.safeSetItem('smk_subjects_v1', JSON.stringify(window.SUBJECT_LIST));
2417
-
2418
- if (firstImportedDate) {
2419
- const ttInput = document.getElementById('timetable-date');
2420
- if (ttInput) ttInput.value = firstImportedDate;
2421
- }
2422
-
2423
- refreshAllUI();
2424
- refreshAllDataViews();
2425
-
2426
- showToast("Logs Restored & Synced",
2427
- `Successfully imported ${importedCount} sessions. Analytics updated.`);
2428
- event.target.value = '';
2429
- };
2430
- reader.readAsText(file);
2431
- }
2432
-
2433
- function handleDBFileUpload(event) {
2434
- const file = event.target.files[0];
2435
- if (!file) return;
2436
-
2437
- const reader = new FileReader();
2438
- reader.onload = function(e) {
2439
- const text = e.target.result;
2440
- const lines = text.split('\n');
2441
-
2442
- let tempRosters = {};
2443
-
2444
- for (let i = 1; i < lines.length; i++) {
2445
- const row = lines[i].split(',').map(s => s.trim());
2446
- if (row.length >= 2) {
2447
- const c = row[0];
2448
- const name = row[1];
2449
- if (c && name) {
2450
- if (!window.ALL_CLASSES.includes(c)) window.ALL_CLASSES.push(c);
2451
- if (!tempRosters[c]) tempRosters[c] = [];
2452
- tempRosters[c].push(name.toUpperCase());
2453
- }
2454
- }
2455
- }
2456
-
2457
- if (Object.keys(tempRosters).length > 0) {
2458
- window.ALL_CLASSES.sort();
2459
- window.rosters = {...window.rosters, ...tempRosters };
2460
- window.safeSetItem('smk_custom_rosters_v3', JSON.stringify(window.rosters));
2461
-
2462
- if (typeof window.syncRostersToCloud === 'function') {
2463
- window.syncRostersToCloud(window.rosters, window.ALL_CLASSES, window.MASTER_TEACHER_LIST, window
2464
- .SUBJECT_LIST);
2465
- }
2466
-
2467
- refreshAllUI();
2468
- showToast("Database Synced", "Class rosters successfully imported.");
2469
- } else {
2470
- alert("No valid class/student data found in CSV file.");
2471
- }
2472
- event.target.value = '';
2473
- };
2474
- reader.readAsText(file);
2475
- }
2476
- </script>
2477
- </body>
2478
- </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>SMK Bandar Kuching No. 1 - Attendance & Roster System</title>
7
  <!-- Tailwind CSS -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
 
9
  <!-- Lucide Icons -->
10
+ <script src="https://unpkg.com/lucide@latest"></script>
 
11
  <!-- Inter Font -->
12
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
13
  <!-- Chart.js for Visual Analytics -->
14
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
15
  <style>
16
+ body { font-family: 'Inter', sans-serif; }
17
+ .status-btn { transition: all 0.2s ease-in-out; }
18
+ .status-btn:active { transform: scale(0.95); }
 
 
 
 
 
 
19
  /* Custom scrollbar styling */
20
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
21
+ ::-webkit-scrollbar-track { background: #f1f5f9; }
22
+ ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
23
+ ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
 
 
 
 
 
 
 
 
 
 
24
  @media print {
25
+ .no-print { display: none !important; }
26
+ .print-only { display: block !important; }
27
+ body { background: white !important; color: black !important; }
 
 
 
 
 
 
 
28
  }
29
  </style>
30
  </head>
 
93
 
94
  <!-- TAB 1: ATTENDANCE TRACKER -->
95
  <div id="tab-tracker" class="space-y-6">
96
+
97
  <!-- Controls Form -->
98
  <div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-5 no-print space-y-4">
99
  <div class="flex items-center justify-between border-b border-slate-100 pb-3 cursor-pointer select-none" onclick="toggleControlsSection()">
 
109
 
110
  <div id="controls-body" class="space-y-4 transition-all duration-300">
111
  <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-6 gap-3">
112
+
113
  <!-- Class Selection -->
114
  <div>
115
  <div class="flex items-center justify-between mb-1.5">
 
178
  </div>
179
  </div>
180
 
181
+ <!-- End Time -->
182
  <div>
183
  <label class="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1.5">6. End Time</label>
184
  <div class="relative">
 
538
 
539
  <!-- Detailed Analysis Tables -->
540
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-2">
541
+
542
  <!-- Pending Submission Table -->
543
  <div class="border border-amber-200 bg-amber-50/20 rounded-xl p-4 space-y-3">
544
  <div class="flex items-center justify-between border-b border-amber-200 pb-2">
 
679
  </div>
680
  </div>
681
  <p class="text-xs text-slate-600 leading-relaxed">
682
+ Are you sure you want to permanently delete all attendance logs and historical sessions?
683
  </p>
684
  <div class="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
685
  <button type="button" id="btn-clear-logs-cancel" class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold text-xs rounded-xl">Cancel</button>
 
792
  function subscribeToLiveSettings() {
793
  if (!currentUser) return;
794
  const settingsDoc = doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'rosters_v1');
795
+
796
  onSnapshot(settingsDoc, (docSnap) => {
797
  if (docSnap.exists()) {
798
  const data = docSnap.data();
799
  let needsRefresh = false;
800
+
801
  if (data.rosters) {
802
  window.safeSetItem('smk_custom_rosters_v3', JSON.stringify(data.rosters));
803
  window.rosters = data.rosters;
 
817
  window.SUBJECT_LIST = data.subjects;
818
  needsRefresh = true;
819
  }
820
+
821
  if (needsRefresh && typeof window.refreshAllUI === 'function') {
822
  window.refreshAllUI();
823
  }
 
830
  window.saveSessionToCloud = async function(sessionRecord) {
831
  if (!currentUser) return;
832
  try {
833
+ const docId = `session_${sessionRecord.id}_${sessionRecord.class}_${sessionRecord.startTime.replace(':','')}`;
834
  const docRef = doc(db, 'artifacts', appId, 'public', 'data', 'sessions', docId);
835
  await setDoc(docRef, sessionRecord);
836
  } catch (err) {
 
841
  window.deleteSessionFromCloud = async function(sessionRecord) {
842
  if (!currentUser) return;
843
  try {
844
+ const docId = `session_${sessionRecord.id}_${sessionRecord.class}_${sessionRecord.startTime.replace(':','')}`;
845
  const docRef = doc(db, 'artifacts', appId, 'public', 'data', 'sessions', docId);
846
  await deleteDoc(docRef);
847
  } catch (err) {
 
853
  if (!currentUser) return;
854
  try {
855
  const docRef = doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'rosters_v1');
856
+ await setDoc(docRef, {
857
+ rosters: rostersData,
858
+ classes: classesData || window.ALL_CLASSES,
859
  teachers: teachersData || window.MASTER_TEACHER_LIST,
860
  subjects: subjectsData || window.SUBJECT_LIST
861
  });
 
916
 
917
  window.SUBJECT_LIST = JSON.parse(window.safeGetItem('smk_subjects_v1', '["Bahasa Melayu", "English", "Mathematics", "Science", "Sejarah", "Geografi", "Pendidikan Islam", "Pendidikan Moral", "RBT", "ASK", "PJK", "Pendidikan Seni Visual", "Fizik", "Kimia", "Biologi", "Matematik Tambahan", "Prinsip Perakaunan", "Ekonomi"]'));
918
  window.MASTER_TEACHER_LIST = JSON.parse(window.safeGetItem('smk_teachers_v1', '["Cikgu Wee", "Cikgu Ahmad", "Cikgu Tan", "Cikgu Dayang", "Cikgu Ling"]'));
919
+
920
+ window.TIME_SLOTS = ["07:00", "07:30", "08:00", "08:30", "09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00", "12:30", "13:00", "13:30"];
 
 
 
 
 
921
  window.ABSENCE_REASONS = ["Tanpa Sebab / Ponteng", "Sakit / Sijil Sakit (MC)", "Kebenaran Sekolah / Aktiviti", "Urusan Keluarga", "Masalah Pengangkutan", "Lain-lain"];
922
 
923
  // Global State variables
 
994
  try { lucide.createIcons(); } catch (e) {}
995
  try { populateDropdowns(); } catch (e) { console.warn('populateDropdowns', e); }
996
  try { setCurrentDate(); } catch (e) { console.warn('setCurrentDate', e); }
997
+ try { setupEventDelegation(); } catch (e) { console.warn('setupEven