Update index.html
Browse files- index.html +84 -24
index.html
CHANGED
|
@@ -151,6 +151,11 @@
|
|
| 151 |
const [teacherFilter, setTeacherFilter] = useState('all');
|
| 152 |
const [searchQuery, setSearchQuery] = useState('');
|
| 153 |
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
const CLASSES = useMemo(() => {
|
| 156 |
const sheetClasses = Object.keys(studentsData || {});
|
|
@@ -164,8 +169,10 @@
|
|
| 164 |
|
| 165 |
const currentTimeSlots = getDynamicTimeSlots(selectedClass);
|
| 166 |
|
| 167 |
-
const
|
| 168 |
-
const
|
|
|
|
|
|
|
| 169 |
|
| 170 |
const selectedClassData = allClassLogs[selectedClass] || {};
|
| 171 |
const teacherLogs = selectedClassData.teacherLogs || {};
|
|
@@ -219,11 +226,11 @@
|
|
| 219 |
return () => clearInterval(interval);
|
| 220 |
}, [fetchLogs]);
|
| 221 |
|
| 222 |
-
const formattedDateStr = `${
|
| 223 |
-
const fallbackDateStr = `${
|
| 224 |
|
| 225 |
let todaysAttendance = attendanceData[formattedDateStr] || attendanceData[fallbackDateStr];
|
| 226 |
-
if (!todaysAttendance && Object.keys(attendanceData).length > 0) {
|
| 227 |
todaysAttendance = attendanceData[Object.keys(attendanceData)[Object.keys(attendanceData).length - 1]];
|
| 228 |
}
|
| 229 |
|
|
@@ -264,23 +271,29 @@
|
|
| 264 |
.sort((a, b) => b[1] - a[1])
|
| 265 |
.slice(0, 3);
|
| 266 |
|
| 267 |
-
const currentHour =
|
| 268 |
-
const currentMin =
|
| 269 |
|
| 270 |
-
const activeTimeSlotObj = getCurrentActiveSlot(selectedClass, currentHour, currentMin);
|
| 271 |
const activeTimeSlotId = activeTimeSlotObj ? activeTimeSlotObj.id : null;
|
| 272 |
|
| 273 |
const lastSlot = currentTimeSlots[currentTimeSlots.length - 1];
|
| 274 |
-
const isAfterSchool =
|
|
|
|
|
|
|
| 275 |
|
| 276 |
const missedPeriods = currentTimeSlots.filter(slot => {
|
| 277 |
if (slot.id === 'R') return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
const hasEnded = currentHour > slot.endHour || (currentHour === slot.endHour && currentMin >= slot.endMin);
|
| 279 |
return hasEnded && !teacherLogs[slot.id];
|
| 280 |
});
|
| 281 |
|
| 282 |
const classesTeacherStatus = CLASSES.map(cls => {
|
| 283 |
-
const activeSlot = getCurrentActiveSlot(cls, currentHour, currentMin);
|
| 284 |
const clsLogs = allClassLogs[cls] || {};
|
| 285 |
const clsTeacherLogs = clsLogs.teacherLogs || {};
|
| 286 |
|
|
@@ -348,7 +361,8 @@
|
|
| 348 |
name: teacher.name,
|
| 349 |
period: slotId,
|
| 350 |
subject: teacher.subject,
|
| 351 |
-
action: 'set'
|
|
|
|
| 352 |
});
|
| 353 |
setIsSaving(false);
|
| 354 |
};
|
|
@@ -367,7 +381,7 @@
|
|
| 367 |
const students = [...selectedStudents];
|
| 368 |
const cls = selectedClass;
|
| 369 |
const logId = Date.now();
|
| 370 |
-
const timeOutStr = `${
|
| 371 |
|
| 372 |
const newLog = {
|
| 373 |
id: logId,
|
|
@@ -393,7 +407,8 @@
|
|
| 393 |
action: 'exit',
|
| 394 |
students: students,
|
| 395 |
timeOut: timeOutStr,
|
| 396 |
-
logId: logId
|
|
|
|
| 397 |
});
|
| 398 |
setIsSaving(false);
|
| 399 |
};
|
|
@@ -417,7 +432,8 @@
|
|
| 417 |
type: 'student',
|
| 418 |
period: slotId,
|
| 419 |
action: 'delete',
|
| 420 |
-
logId: logId
|
|
|
|
| 421 |
});
|
| 422 |
} else if (toDelete.type === 'teacher') {
|
| 423 |
const { slotId } = toDelete;
|
|
@@ -432,7 +448,8 @@
|
|
| 432 |
className: selectedClass,
|
| 433 |
type: 'teacher',
|
| 434 |
period: slotId,
|
| 435 |
-
action: 'delete'
|
|
|
|
| 436 |
});
|
| 437 |
}
|
| 438 |
setIsSaving(false);
|
|
@@ -462,7 +479,8 @@
|
|
| 462 |
name: prevLog.name,
|
| 463 |
period: currentSlotId,
|
| 464 |
subject: prevLog.subject,
|
| 465 |
-
action: 'set'
|
|
|
|
| 466 |
});
|
| 467 |
setIsSaving(false);
|
| 468 |
}
|
|
@@ -499,9 +517,39 @@
|
|
| 499 |
</div>
|
| 500 |
|
| 501 |
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 text-sm">
|
| 502 |
-
<div className="bg-indigo-800/60 backdrop-blur-md px-3 py-2 rounded-lg border border-indigo-500/30 flex items-center gap-2 justify-center">
|
| 503 |
-
<Calendar className="w-4 h-4 text-indigo-300" />
|
| 504 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
</div>
|
| 506 |
|
| 507 |
{activeTab === 'class_view' && (
|
|
@@ -621,13 +669,25 @@
|
|
| 621 |
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 border-b border-slate-100 pb-4">
|
| 622 |
<div>
|
| 623 |
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
| 624 |
-
<Activity className="w-5 h-5 text-indigo-600" /> Status Guru Semasa (Real-Time)
|
| 625 |
</h2>
|
| 626 |
<p className="text-xs text-slate-500 mt-0.5">
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
</p>
|
| 632 |
</div>
|
| 633 |
|
|
|
|
| 151 |
const [teacherFilter, setTeacherFilter] = useState('all');
|
| 152 |
const [searchQuery, setSearchQuery] = useState('');
|
| 153 |
const [isSaving, setIsSaving] = useState(false);
|
| 154 |
+
const [selectedDate, setSelectedDate] = useState(() => {
|
| 155 |
+
const d = new Date();
|
| 156 |
+
d.setHours(0, 0, 0, 0);
|
| 157 |
+
return d;
|
| 158 |
+
});
|
| 159 |
|
| 160 |
const CLASSES = useMemo(() => {
|
| 161 |
const sheetClasses = Object.keys(studentsData || {});
|
|
|
|
| 169 |
|
| 170 |
const currentTimeSlots = getDynamicTimeSlots(selectedClass);
|
| 171 |
|
| 172 |
+
const now = new Date();
|
| 173 |
+
const isToday = selectedDate.toDateString() === now.toDateString();
|
| 174 |
+
const dateId = `${selectedDate.getDate().toString().padStart(2, '0')}-${(selectedDate.getMonth() + 1).toString().padStart(2, '0')}-${selectedDate.getFullYear()}`;
|
| 175 |
+
const selectedDateInputValue = `${selectedDate.getFullYear()}-${(selectedDate.getMonth() + 1).toString().padStart(2, '0')}-${selectedDate.getDate().toString().padStart(2, '0')}`;
|
| 176 |
|
| 177 |
const selectedClassData = allClassLogs[selectedClass] || {};
|
| 178 |
const teacherLogs = selectedClassData.teacherLogs || {};
|
|
|
|
| 226 |
return () => clearInterval(interval);
|
| 227 |
}, [fetchLogs]);
|
| 228 |
|
| 229 |
+
const formattedDateStr = `${selectedDate.getDate().toString().padStart(2, '0')}/${(selectedDate.getMonth() + 1).toString().padStart(2, '0')}/${selectedDate.getFullYear()}`;
|
| 230 |
+
const fallbackDateStr = `${selectedDate.getDate()}/${selectedDate.getMonth() + 1}/${selectedDate.getFullYear()}`;
|
| 231 |
|
| 232 |
let todaysAttendance = attendanceData[formattedDateStr] || attendanceData[fallbackDateStr];
|
| 233 |
+
if (!todaysAttendance && isToday && Object.keys(attendanceData).length > 0) {
|
| 234 |
todaysAttendance = attendanceData[Object.keys(attendanceData)[Object.keys(attendanceData).length - 1]];
|
| 235 |
}
|
| 236 |
|
|
|
|
| 271 |
.sort((a, b) => b[1] - a[1])
|
| 272 |
.slice(0, 3);
|
| 273 |
|
| 274 |
+
const currentHour = now.getHours();
|
| 275 |
+
const currentMin = now.getMinutes();
|
| 276 |
|
| 277 |
+
const activeTimeSlotObj = isToday ? getCurrentActiveSlot(selectedClass, currentHour, currentMin) : null;
|
| 278 |
const activeTimeSlotId = activeTimeSlotObj ? activeTimeSlotObj.id : null;
|
| 279 |
|
| 280 |
const lastSlot = currentTimeSlots[currentTimeSlots.length - 1];
|
| 281 |
+
const isAfterSchool = isToday
|
| 282 |
+
? (currentHour > lastSlot.endHour || (currentHour === lastSlot.endHour && currentMin >= lastSlot.endMin))
|
| 283 |
+
: (selectedDate < now);
|
| 284 |
|
| 285 |
const missedPeriods = currentTimeSlots.filter(slot => {
|
| 286 |
if (slot.id === 'R') return false;
|
| 287 |
+
if (!isToday) {
|
| 288 |
+
// For past/future dates: treat as missed if no teacher log
|
| 289 |
+
return !teacherLogs[slot.id];
|
| 290 |
+
}
|
| 291 |
const hasEnded = currentHour > slot.endHour || (currentHour === slot.endHour && currentMin >= slot.endMin);
|
| 292 |
return hasEnded && !teacherLogs[slot.id];
|
| 293 |
});
|
| 294 |
|
| 295 |
const classesTeacherStatus = CLASSES.map(cls => {
|
| 296 |
+
const activeSlot = isToday ? getCurrentActiveSlot(cls, currentHour, currentMin) : null;
|
| 297 |
const clsLogs = allClassLogs[cls] || {};
|
| 298 |
const clsTeacherLogs = clsLogs.teacherLogs || {};
|
| 299 |
|
|
|
|
| 361 |
name: teacher.name,
|
| 362 |
period: slotId,
|
| 363 |
subject: teacher.subject,
|
| 364 |
+
action: 'set',
|
| 365 |
+
date: dateId
|
| 366 |
});
|
| 367 |
setIsSaving(false);
|
| 368 |
};
|
|
|
|
| 381 |
const students = [...selectedStudents];
|
| 382 |
const cls = selectedClass;
|
| 383 |
const logId = Date.now();
|
| 384 |
+
const timeOutStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
| 385 |
|
| 386 |
const newLog = {
|
| 387 |
id: logId,
|
|
|
|
| 407 |
action: 'exit',
|
| 408 |
students: students,
|
| 409 |
timeOut: timeOutStr,
|
| 410 |
+
logId: logId,
|
| 411 |
+
date: dateId
|
| 412 |
});
|
| 413 |
setIsSaving(false);
|
| 414 |
};
|
|
|
|
| 432 |
type: 'student',
|
| 433 |
period: slotId,
|
| 434 |
action: 'delete',
|
| 435 |
+
logId: logId,
|
| 436 |
+
date: dateId
|
| 437 |
});
|
| 438 |
} else if (toDelete.type === 'teacher') {
|
| 439 |
const { slotId } = toDelete;
|
|
|
|
| 448 |
className: selectedClass,
|
| 449 |
type: 'teacher',
|
| 450 |
period: slotId,
|
| 451 |
+
action: 'delete',
|
| 452 |
+
date: dateId
|
| 453 |
});
|
| 454 |
}
|
| 455 |
setIsSaving(false);
|
|
|
|
| 479 |
name: prevLog.name,
|
| 480 |
period: currentSlotId,
|
| 481 |
subject: prevLog.subject,
|
| 482 |
+
action: 'set',
|
| 483 |
+
date: dateId
|
| 484 |
});
|
| 485 |
setIsSaving(false);
|
| 486 |
}
|
|
|
|
| 517 |
</div>
|
| 518 |
|
| 519 |
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 text-sm">
|
| 520 |
+
<div className="bg-indigo-800/60 backdrop-blur-md px-3 py-2 rounded-lg border border-indigo-500/30 flex items-center gap-2 justify-center relative">
|
| 521 |
+
<Calendar className="w-4 h-4 text-indigo-300 flex-shrink-0" />
|
| 522 |
+
<label className="cursor-pointer flex items-center gap-2">
|
| 523 |
+
<span className="whitespace-nowrap">
|
| 524 |
+
{selectedDate.toLocaleDateString('ms-MY', { weekday: 'long', day: 'numeric', month: 'short', year: 'numeric' })}
|
| 525 |
+
</span>
|
| 526 |
+
<input
|
| 527 |
+
type="date"
|
| 528 |
+
value={selectedDateInputValue}
|
| 529 |
+
onChange={(e) => {
|
| 530 |
+
if (!e.target.value) return;
|
| 531 |
+
const [y, m, d] = e.target.value.split('-').map(Number);
|
| 532 |
+
const newDate = new Date(y, m - 1, d);
|
| 533 |
+
newDate.setHours(0, 0, 0, 0);
|
| 534 |
+
setSelectedDate(newDate);
|
| 535 |
+
}}
|
| 536 |
+
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
| 537 |
+
title="Pilih tarikh"
|
| 538 |
+
/>
|
| 539 |
+
</label>
|
| 540 |
+
{!isToday && (
|
| 541 |
+
<button
|
| 542 |
+
onClick={() => {
|
| 543 |
+
const d = new Date();
|
| 544 |
+
d.setHours(0, 0, 0, 0);
|
| 545 |
+
setSelectedDate(d);
|
| 546 |
+
}}
|
| 547 |
+
className="ml-1 text-[10px] font-bold bg-white/20 hover:bg-white/30 px-1.5 py-0.5 rounded text-white whitespace-nowrap"
|
| 548 |
+
title="Kembali ke hari ini"
|
| 549 |
+
>
|
| 550 |
+
Hari Ini
|
| 551 |
+
</button>
|
| 552 |
+
)}
|
| 553 |
</div>
|
| 554 |
|
| 555 |
{activeTab === 'class_view' && (
|
|
|
|
| 669 |
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 border-b border-slate-100 pb-4">
|
| 670 |
<div>
|
| 671 |
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
|
| 672 |
+
<Activity className="w-5 h-5 text-indigo-600" /> Status Guru {isToday ? 'Semasa (Real-Time)' : 'Mengikut Tarikh Dipilih'}
|
| 673 |
</h2>
|
| 674 |
<p className="text-xs text-slate-500 mt-0.5">
|
| 675 |
+
{isToday ? (
|
| 676 |
+
<>
|
| 677 |
+
Memaparkan status guru mengajar di setiap kelas mengikut waktu jam sekarang (
|
| 678 |
+
<span className="font-bold text-indigo-700">
|
| 679 |
+
{now.toLocaleTimeString('ms-MY', { hour: '2-digit', minute: '2-digit' })}
|
| 680 |
+
</span>)
|
| 681 |
+
</>
|
| 682 |
+
) : (
|
| 683 |
+
<>
|
| 684 |
+
Memaparkan rekod guru untuk tarikh{' '}
|
| 685 |
+
<span className="font-bold text-indigo-700">
|
| 686 |
+
{selectedDate.toLocaleDateString('ms-MY', { weekday: 'long', day: 'numeric', month: 'short', year: 'numeric' })}
|
| 687 |
+
</span>
|
| 688 |
+
{' '}(bukan hari ini)
|
| 689 |
+
</>
|
| 690 |
+
)}
|
| 691 |
</p>
|
| 692 |
</div>
|
| 693 |
|