Update index.html
Browse files- index.html +71 -45
index.html
CHANGED
|
@@ -229,31 +229,37 @@
|
|
| 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 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
}
|
| 236 |
|
| 237 |
-
const absenteesForClass =
|
| 238 |
const classStudents = studentsData[selectedClass] || [];
|
| 239 |
-
const totalStudents = classStudents.length || 24;
|
| 240 |
-
|
| 241 |
-
const
|
| 242 |
-
const
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
let schoolTotalEnrolled = 0;
|
| 245 |
let schoolTotalAbsent = 0;
|
|
|
|
| 246 |
|
| 247 |
CLASSES.forEach(cls => {
|
| 248 |
const cStudents = studentsData[cls] || [];
|
| 249 |
-
const
|
| 250 |
-
schoolTotalEnrolled +=
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
});
|
| 253 |
-
const
|
| 254 |
-
const schoolAttendancePercentage = schoolTotalEnrolled > 0
|
| 255 |
? Math.round((schoolTotalPresent / schoolTotalEnrolled) * 100)
|
| 256 |
-
:
|
| 257 |
|
| 258 |
const exitCounts = {};
|
| 259 |
Object.values(studentLogs).forEach(slotLogs => {
|
|
@@ -311,11 +317,13 @@
|
|
| 311 |
}
|
| 312 |
|
| 313 |
const cStudents = studentsData[cls] || [];
|
| 314 |
-
const cAbsents =
|
| 315 |
const cEnrolled = cStudents.length || 24;
|
| 316 |
-
const cAbsentCount = cAbsents.length;
|
| 317 |
-
const cPresentCount = cEnrolled - cAbsentCount;
|
| 318 |
-
const cPercentage =
|
|
|
|
|
|
|
| 319 |
|
| 320 |
return {
|
| 321 |
className: cls,
|
|
@@ -517,34 +525,33 @@
|
|
| 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
|
| 521 |
<Calendar className="w-4 h-4 text-indigo-300 flex-shrink-0" />
|
| 522 |
-
<
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 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="
|
| 548 |
title="Kembali ke hari ini"
|
| 549 |
>
|
| 550 |
Hari Ini
|
|
@@ -857,7 +864,9 @@
|
|
| 857 |
<>
|
| 858 |
<div className="p-4 md:p-6 grid grid-cols-1 lg:grid-cols-2 gap-6 border-b border-slate-100 bg-white">
|
| 859 |
<div className="space-y-3">
|
| 860 |
-
<h2 className="text-xs font-bold uppercase tracking-wider text-slate-500">
|
|
|
|
|
|
|
| 861 |
<div className="grid grid-cols-2 gap-3">
|
| 862 |
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 flex items-center justify-between">
|
| 863 |
<span className="text-xs md:text-sm font-medium text-slate-600">Hadir:</span>
|
|
@@ -872,6 +881,9 @@
|
|
| 872 |
<span className="text-xs md:text-sm font-medium text-slate-600">Jumlah Daftar:</span>
|
| 873 |
<span className="text-base md:text-lg font-bold text-slate-800">{totalStudents}</span>
|
| 874 |
</div>
|
|
|
|
|
|
|
|
|
|
| 875 |
</div>
|
| 876 |
|
| 877 |
<div>
|
|
@@ -879,6 +891,11 @@
|
|
| 879 |
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 min-h-[120px] flex flex-col justify-center">
|
| 880 |
{isLoading ? (
|
| 881 |
<div className="text-center text-slate-400 text-sm italic">Memuatkan data...</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 882 |
) : absenteesForClass && absenteesForClass.length > 0 ? (
|
| 883 |
<ul className="text-sm space-y-1.5 max-h-32 overflow-y-auto pr-1">
|
| 884 |
{absenteesForClass.map((student, idx) => (
|
|
@@ -904,18 +921,27 @@
|
|
| 904 |
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
| 905 |
|
| 906 |
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
| 907 |
-
<div className="text-xs font-bold text-slate-500 mb-1">
|
|
|
|
|
|
|
| 908 |
<div className="flex items-baseline gap-2 mb-2">
|
| 909 |
-
<span className={`text-2xl md:text-3xl font-black ${
|
| 910 |
-
|
|
|
|
|
|
|
| 911 |
</span>
|
| 912 |
</div>
|
| 913 |
<div className="w-full bg-slate-100 rounded-full h-2.5">
|
| 914 |
<div
|
| 915 |
-
className={`h-2.5 rounded-full ${
|
| 916 |
-
|
|
|
|
|
|
|
| 917 |
></div>
|
| 918 |
</div>
|
|
|
|
|
|
|
|
|
|
| 919 |
</div>
|
| 920 |
|
| 921 |
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
|
|
|
| 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 |
+
// Only use attendance data that matches the selected date — no fallback to other days
|
| 233 |
+
const todaysAttendance = attendanceData[formattedDateStr] || attendanceData[fallbackDateStr] || null;
|
| 234 |
+
const hasAttendanceData = !!todaysAttendance;
|
|
|
|
| 235 |
|
| 236 |
+
const absenteesForClass = hasAttendanceData ? (todaysAttendance[selectedClass] || []) : [];
|
| 237 |
const classStudents = studentsData[selectedClass] || [];
|
| 238 |
+
const totalStudents = classStudents.length || 24;
|
| 239 |
+
// If no attendance record for this date (e.g. Saturday / public holiday), show 0 present & 0 absent
|
| 240 |
+
const absentCount = hasAttendanceData ? absenteesForClass.length : 0;
|
| 241 |
+
const presentCount = hasAttendanceData ? (totalStudents - absentCount) : 0;
|
| 242 |
+
const attendancePercentage = hasAttendanceData && totalStudents > 0
|
| 243 |
+
? Math.round((presentCount / totalStudents) * 100)
|
| 244 |
+
: 0;
|
| 245 |
|
| 246 |
let schoolTotalEnrolled = 0;
|
| 247 |
let schoolTotalAbsent = 0;
|
| 248 |
+
let schoolTotalPresent = 0;
|
| 249 |
|
| 250 |
CLASSES.forEach(cls => {
|
| 251 |
const cStudents = studentsData[cls] || [];
|
| 252 |
+
const cEnrolled = cStudents.length || 24;
|
| 253 |
+
schoolTotalEnrolled += cEnrolled;
|
| 254 |
+
if (hasAttendanceData) {
|
| 255 |
+
const cAbsents = todaysAttendance[cls] || [];
|
| 256 |
+
schoolTotalAbsent += cAbsents.length;
|
| 257 |
+
schoolTotalPresent += (cEnrolled - cAbsents.length);
|
| 258 |
+
}
|
| 259 |
});
|
| 260 |
+
const schoolAttendancePercentage = hasAttendanceData && schoolTotalEnrolled > 0
|
|
|
|
| 261 |
? Math.round((schoolTotalPresent / schoolTotalEnrolled) * 100)
|
| 262 |
+
: 0;
|
| 263 |
|
| 264 |
const exitCounts = {};
|
| 265 |
Object.values(studentLogs).forEach(slotLogs => {
|
|
|
|
| 317 |
}
|
| 318 |
|
| 319 |
const cStudents = studentsData[cls] || [];
|
| 320 |
+
const cAbsents = hasAttendanceData ? (todaysAttendance[cls] || []) : [];
|
| 321 |
const cEnrolled = cStudents.length || 24;
|
| 322 |
+
const cAbsentCount = hasAttendanceData ? cAbsents.length : 0;
|
| 323 |
+
const cPresentCount = hasAttendanceData ? (cEnrolled - cAbsentCount) : 0;
|
| 324 |
+
const cPercentage = hasAttendanceData && cEnrolled > 0
|
| 325 |
+
? Math.round((cPresentCount / cEnrolled) * 100)
|
| 326 |
+
: 0;
|
| 327 |
|
| 328 |
return {
|
| 329 |
className: cls,
|
|
|
|
| 525 |
</div>
|
| 526 |
|
| 527 |
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 text-sm">
|
| 528 |
+
<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 flex-wrap">
|
| 529 |
<Calendar className="w-4 h-4 text-indigo-300 flex-shrink-0" />
|
| 530 |
+
<input
|
| 531 |
+
type="date"
|
| 532 |
+
value={selectedDateInputValue}
|
| 533 |
+
onChange={(e) => {
|
| 534 |
+
if (!e.target.value) return;
|
| 535 |
+
const [y, m, d] = e.target.value.split('-').map(Number);
|
| 536 |
+
const newDate = new Date(y, m - 1, d);
|
| 537 |
+
newDate.setHours(0, 0, 0, 0);
|
| 538 |
+
setSelectedDate(newDate);
|
| 539 |
+
}}
|
| 540 |
+
className="bg-indigo-900 border border-indigo-400/40 rounded px-2 py-1 text-white font-bold focus:outline-none cursor-pointer text-sm [color-scheme:dark]"
|
| 541 |
+
title="Pilih tarikh"
|
| 542 |
+
/>
|
| 543 |
+
<span className="text-indigo-200 text-xs hidden sm:inline whitespace-nowrap">
|
| 544 |
+
{selectedDate.toLocaleDateString('ms-MY', { weekday: 'long' })}
|
| 545 |
+
</span>
|
|
|
|
|
|
|
| 546 |
{!isToday && (
|
| 547 |
<button
|
| 548 |
+
type="button"
|
| 549 |
onClick={() => {
|
| 550 |
const d = new Date();
|
| 551 |
d.setHours(0, 0, 0, 0);
|
| 552 |
setSelectedDate(d);
|
| 553 |
}}
|
| 554 |
+
className="text-[10px] font-bold bg-white/20 hover:bg-white/30 px-1.5 py-0.5 rounded text-white whitespace-nowrap"
|
| 555 |
title="Kembali ke hari ini"
|
| 556 |
>
|
| 557 |
Hari Ini
|
|
|
|
| 864 |
<>
|
| 865 |
<div className="p-4 md:p-6 grid grid-cols-1 lg:grid-cols-2 gap-6 border-b border-slate-100 bg-white">
|
| 866 |
<div className="space-y-3">
|
| 867 |
+
<h2 className="text-xs font-bold uppercase tracking-wider text-slate-500">
|
| 868 |
+
Kehadiran {isToday ? 'Hari Ini' : 'Tarikh Dipilih'} ({selectedClass})
|
| 869 |
+
</h2>
|
| 870 |
<div className="grid grid-cols-2 gap-3">
|
| 871 |
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 flex items-center justify-between">
|
| 872 |
<span className="text-xs md:text-sm font-medium text-slate-600">Hadir:</span>
|
|
|
|
| 881 |
<span className="text-xs md:text-sm font-medium text-slate-600">Jumlah Daftar:</span>
|
| 882 |
<span className="text-base md:text-lg font-bold text-slate-800">{totalStudents}</span>
|
| 883 |
</div>
|
| 884 |
+
{!hasAttendanceData && (
|
| 885 |
+
<p className="text-[11px] text-amber-600 font-medium italic">Tiada data kehadiran untuk tarikh ini</p>
|
| 886 |
+
)}
|
| 887 |
</div>
|
| 888 |
|
| 889 |
<div>
|
|
|
|
| 891 |
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 min-h-[120px] flex flex-col justify-center">
|
| 892 |
{isLoading ? (
|
| 893 |
<div className="text-center text-slate-400 text-sm italic">Memuatkan data...</div>
|
| 894 |
+
) : !hasAttendanceData ? (
|
| 895 |
+
<div className="flex flex-col items-center justify-center text-slate-400 italic">
|
| 896 |
+
<span className="text-sm">Tiada data kehadiran</span>
|
| 897 |
+
<span className="text-[11px] mt-1">(contoh: hari cuti / hujung minggu)</span>
|
| 898 |
+
</div>
|
| 899 |
) : absenteesForClass && absenteesForClass.length > 0 ? (
|
| 900 |
<ul className="text-sm space-y-1.5 max-h-32 overflow-y-auto pr-1">
|
| 901 |
{absenteesForClass.map((student, idx) => (
|
|
|
|
| 921 |
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
| 922 |
|
| 923 |
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
| 924 |
+
<div className="text-xs font-bold text-slate-500 mb-1">
|
| 925 |
+
Kadar Kehadiran {hasAttendanceData ? `(${presentCount}/${totalStudents})` : ''}
|
| 926 |
+
</div>
|
| 927 |
<div className="flex items-baseline gap-2 mb-2">
|
| 928 |
+
<span className={`text-2xl md:text-3xl font-black ${
|
| 929 |
+
!hasAttendanceData ? 'text-slate-400' : attendancePercentage < 80 ? 'text-orange-500' : 'text-emerald-500'
|
| 930 |
+
}`}>
|
| 931 |
+
{hasAttendanceData ? `${attendancePercentage}%` : '—'}
|
| 932 |
</span>
|
| 933 |
</div>
|
| 934 |
<div className="w-full bg-slate-100 rounded-full h-2.5">
|
| 935 |
<div
|
| 936 |
+
className={`h-2.5 rounded-full ${
|
| 937 |
+
!hasAttendanceData ? 'bg-slate-300' : attendancePercentage < 80 ? 'bg-orange-500' : 'bg-emerald-500'
|
| 938 |
+
}`}
|
| 939 |
+
style={{ width: `${hasAttendanceData ? attendancePercentage : 0}%` }}
|
| 940 |
></div>
|
| 941 |
</div>
|
| 942 |
+
{!hasAttendanceData && (
|
| 943 |
+
<p className="text-[10px] text-slate-400 italic mt-1">Tiada data</p>
|
| 944 |
+
)}
|
| 945 |
</div>
|
| 946 |
|
| 947 |
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|