testing / index.html
SMKBK1's picture
Update index.html
40f85cf verified
Raw History Blame Contribute Delete
69.5 kB
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-Buku Kawalan Bersama</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
</style>
</head>
<body class="bg-slate-100 min-h-screen text-slate-800 flex flex-col justify-between">
<div id="root" class="flex-grow"></div>
<script type="text/babel">
const { useState, useEffect, useMemo, useCallback } = React;
const Icon = ({ path, className = "w-5 h-5", viewBox = "0 0 24 24" }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox={viewBox} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{path}
</svg>
);
const BookOpen = (p) => <Icon {...p} path={<><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></>} />;
const Calendar = (p) => <Icon {...p} path={<><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></>} />;
const Users = (p) => <Icon {...p} path={<><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></>} />;
const AlertTriangle = (p) => <Icon {...p} path={<><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></>} />;
const PieChart = (p) => <Icon {...p} path={<><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></>} />;
const Activity = (p) => <Icon {...p} path={<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>} />;
const Clock = (p) => <Icon {...p} path={<><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></>} />;
const CheckSquare = (p) => <Icon {...p} path={<><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></>} />;
const Trash2 = (p) => <Icon {...p} path={<><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></>} />;
const CloudOff = (p) => <Icon {...p} path={<><line x1="1" y1="1" x2="23" y2="23"/><path d="M22.61 16.95A5 5 0 0 0 18 10h-1.26a8 8 0 0 0-7.05-6M5 5a8 8 0 0 0-4 7h.17A5 5 0 0 0 5 22h13"/></>} />;
const Copy = (p) => <Icon {...p} path={<><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></>} />;
const LayoutDashboard = (p) => <Icon {...p} path={<><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="15" width="7" height="6"/></>} />;
const UserX = (p) => <Icon {...p} path={<><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="18" y1="8" x2="23" y2="13"/><line x1="23" y1="8" x2="18" y2="13"/></>} />;
const UserCheck = (p) => <Icon {...p} path={<><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><polyline points="17 11 19 13 23 9"/></>} />;
const ShieldAlert = (p) => <Icon {...p} path={<><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></>} />;
const ArrowRight = (p) => <Icon {...p} path={<><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></>} />;
const GOOGLE_SHEETS_WEB_APP_URL = 'https://script.google.com/macros/s/AKfycbyW8HovLImrWBDRqG_nL50DrotcTuHQbYAm5kW2HNd1_PKLwZPO4l6P9pjGCdk4T4M/exec';
const LOGS_WEB_APP_URL = 'https://script.google.com/macros/s/AKfycbw8neGdrM0tScM5A2bCK4DamtkA9A_-TvCIgQ8PrcXJPxqEZ5nudr9_FLjp9zyStWBm/exec';
const DEFAULT_CLASSES = [
'PA',
'1A', '1B', '1C', '1D', '1E',
'2A', '2B', '2C', '2D', '2E',
'3A', '3B', '3C', '3D', '3E',
'4A', '4B', '4C', '4D', '4E',
'5A', '5B', '5C', '5D', '5E'
];
const getDynamicTimeSlots = (className) => {
const isLowerForm = ['P', '1', '2', '3'].includes(className.charAt(0));
const slots = [
{ id: 'GT', label: 'GT', time: '0650-0700', endHour: 7, endMin: 0 },
{ id: '1', label: '1', time: '0700-0730', endHour: 7, endMin: 30 },
{ id: '2', label: '2', time: '0730-0800', endHour: 8, endMin: 0 },
{ id: '3', label: '3', time: '0800-0830', endHour: 8, endMin: 30 },
{ id: '4', label: '4', time: '0830-0900', endHour: 9, endMin: 0 },
{ id: '5', label: '5', time: '0900-0930', endHour: 9, endMin: 30 },
];
if (isLowerForm) {
slots.push({ id: 'R', label: 'REHAT', time: '0930-1000', endHour: 10, endMin: 0 });
slots.push({ id: '6', label: '6', time: '1000-1030', endHour: 10, endMin: 30 });
} else {
slots.push({ id: '6', label: '6', time: '0930-1000', endHour: 10, endMin: 0 });
slots.push({ id: 'R', label: 'REHAT', time: '1000-1030', endHour: 10, endMin: 30 });
}
slots.push(
{ id: '7', label: '7', time: '1030-1100', endHour: 11, endMin: 0 },
{ id: '8', label: '8', time: '1100-1130', endHour: 11, endMin: 30 },
{ id: '9', label: '9', time: '1130-1200', endHour: 12, endMin: 0 },
{ id: '10', label: '10', time: '1200-1230', endHour: 12, endMin: 30 },
{ id: '11', label: '11', time: '1230-1300', endHour: 13, endMin: 0 },
{ id: '12', label: '12', time: '1300-1330', endHour: 13, endMin: 30 },
{ id: '13', label: '13', time: '1330-1400', endHour: 14, endMin: 0 }
);
return slots;
};
const getCurrentActiveSlot = (className, hour, min) => {
const slots = getDynamicTimeSlots(className);
const currentMins = hour * 60 + min;
for (const slot of slots) {
const [startStr, endStr] = slot.time.split('-');
const startMins = parseInt(startStr.slice(0, 2)) * 60 + parseInt(startStr.slice(2, 4));
const endMins = parseInt(endStr.slice(0, 2)) * 60 + parseInt(endStr.slice(2, 4));
if (currentMins >= startMins && currentMins < endMins) {
return slot;
}
}
return null;
};
const postLog = async (payload) => {
try {
await fetch(LOGS_WEB_APP_URL, {
method: 'POST',
mode: 'no-cors',
headers: { 'Content-Type': 'text/plain' },
body: JSON.stringify(payload)
});
return true;
} catch (err) {
console.error('Post log error:', err);
return false;
}
};
const MALAYSIA_TIME_ZONE = 'Asia/Kuala_Lumpur';
const getMalaysiaParts = (date = new Date()) => {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: MALAYSIA_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23'
}).formatToParts(date);
const get = (type) => parts.find(p => p.type === type)?.value || '00';
return {
year: Number(get('year')),
month: Number(get('month')),
day: Number(get('day')),
hour: Number(get('hour')),
minute: Number(get('minute')),
second: Number(get('second'))
};
};
const getMalaysiaDateId = (date = new Date()) => {
const p = getMalaysiaParts(date);
return `${p.day.toString().padStart(2, '0')}-${p.month.toString().padStart(2, '0')}-${p.year}`;
};
const getMalaysiaDateInputValue = (date = new Date()) => {
const p = getMalaysiaParts(date);
return `${p.year}-${p.month.toString().padStart(2, '0')}-${p.day.toString().padStart(2, '0')}`;
};
const getMalaysiaTimeString = (date = new Date()) => {
const p = getMalaysiaParts(date);
return `${p.hour.toString().padStart(2, '0')}:${p.minute.toString().padStart(2, '0')}`;
};
const getMalaysiaDateObject = (date = new Date()) => {
const p = getMalaysiaParts(date);
return new Date(p.year, p.month - 1, p.day);
};
function App() {
const [isOnline, setIsOnline] = useState(false);
const [activeTab, setActiveTab] = useState('class_view');
const [selectedClass, setSelectedClass] = useState('5B');
const [teachersData, setTeachersData] = useState([]);
const [subjectsData, setSubjectsData] = useState([]);
const [studentsData, setStudentsData] = useState({});
const [attendanceData, setAttendanceData] = useState({});
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [allClassLogs, setAllClassLogs] = useState({});
const [activeSlot, setActiveSlot] = useState(null);
const [isTeacherModalOpen, setIsTeacherModalOpen] = useState(false);
const [isStudentModalOpen, setIsStudentModalOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
const [tempTeacher, setTempTeacher] = useState({ subject: '', name: '' });
const [selectedStudents, setSelectedStudents] = useState([]);
const [teacherFilter, setTeacherFilter] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [selectedDate, setSelectedDate] = useState(() => getMalaysiaDateObject());
const CLASSES = useMemo(() => {
const sheetClasses = Object.keys(studentsData || {});
if (sheetClasses.length > 0) {
return Array.from(new Set([...sheetClasses, ...DEFAULT_CLASSES])).sort((a, b) => {
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
});
}
return DEFAULT_CLASSES;
}, [studentsData]);
const currentTimeSlots = getDynamicTimeSlots(selectedClass);
const now = new Date();
const malaysiaNow = getMalaysiaParts(now);
const malaysiaToday = getMalaysiaDateObject(now);
const isToday = selectedDate.getTime() === malaysiaToday.getTime();
const dateId = getMalaysiaDateId(selectedDate);
const selectedDateInputValue = getMalaysiaDateInputValue(selectedDate);
const selectedClassData = allClassLogs[selectedClass] || {};
const teacherLogs = selectedClassData.teacherLogs || {};
const studentLogs = selectedClassData.studentLogs || {};
useEffect(() => {
const fetchStaticData = async () => {
setIsLoading(true);
try {
const response = await fetch(GOOGLE_SHEETS_WEB_APP_URL);
const data = await response.json();
if (data.status === 'success') {
setTeachersData(data.teachers || []);
setSubjectsData(data.subjects || []);
setStudentsData(data.students || {});
setAttendanceData(data.attendance || {});
setError(null);
} else {
setError("Gagal memuatkan data sekolah.");
}
} catch (e) {
setError("Gagal menyambung ke pangkalan data sekolah.");
} finally {
setIsLoading(false);
}
};
fetchStaticData();
}, []);
const fetchLogs = useCallback(async (showLoading = false) => {
try {
const url = `${LOGS_WEB_APP_URL}?date=${dateId}&t=${Date.now()}`;
const response = await fetch(url);
const data = await response.json();
if (data.status === 'success') {
setAllClassLogs(data.logs || {});
setIsOnline(true);
setError(null);
} else {
setIsOnline(false);
}
} catch (e) {
console.error('Fetch logs error:', e);
setIsOnline(false);
}
}, [dateId]);
useEffect(() => {
fetchLogs();
const interval = setInterval(() => fetchLogs(), 30000);
return () => clearInterval(interval);
}, [fetchLogs]);
const formattedDateStr = `${selectedDate.getDate().toString().padStart(2, '0')}/${(selectedDate.getMonth() + 1).toString().padStart(2, '0')}/${selectedDate.getFullYear()}`;
const fallbackDateStr = `${selectedDate.getDate()}/${selectedDate.getMonth() + 1}/${selectedDate.getFullYear()}`;
// Only use attendance data that matches the selected date — no fallback to other days
const todaysAttendance = attendanceData[formattedDateStr] || attendanceData[fallbackDateStr] || null;
const hasAttendanceData = !!todaysAttendance;
const absenteesForClass = hasAttendanceData ? (todaysAttendance[selectedClass] || []) : [];
const classStudents = studentsData[selectedClass] || [];
const totalStudents = classStudents.length || 24;
// If no attendance record for this date (e.g. Saturday / public holiday), show 0 present & 0 absent
const absentCount = hasAttendanceData ? absenteesForClass.length : 0;
const presentCount = hasAttendanceData ? (totalStudents - absentCount) : 0;
const attendancePercentage = hasAttendanceData && totalStudents > 0
? Math.round((presentCount / totalStudents) * 100)
: 0;
let schoolTotalEnrolled = 0;
let schoolTotalAbsent = 0;
let schoolTotalPresent = 0;
CLASSES.forEach(cls => {
const cStudents = studentsData[cls] || [];
const cEnrolled = cStudents.length || 24;
schoolTotalEnrolled += cEnrolled;
if (hasAttendanceData) {
const cAbsents = todaysAttendance[cls] || [];
schoolTotalAbsent += cAbsents.length;
schoolTotalPresent += (cEnrolled - cAbsents.length);
}
});
const schoolAttendancePercentage = hasAttendanceData && schoolTotalEnrolled > 0
? Math.round((schoolTotalPresent / schoolTotalEnrolled) * 100)
: 0;
const exitCounts = {};
Object.values(studentLogs).forEach(slotLogs => {
if (Array.isArray(slotLogs)) {
slotLogs.forEach(log => {
if (log.names) {
log.names.forEach(name => {
exitCounts[name] = (exitCounts[name] || 0) + 1;
});
}
});
}
});
const frequentExits = Object.entries(exitCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
const currentHour = malaysiaNow.hour;
const currentMin = malaysiaNow.minute;
const activeTimeSlotObj = isToday ? getCurrentActiveSlot(selectedClass, currentHour, currentMin) : null;
const activeTimeSlotId = activeTimeSlotObj ? activeTimeSlotObj.id : null;
const lastSlot = currentTimeSlots[currentTimeSlots.length - 1];
const isAfterSchool = isToday
? (currentHour > lastSlot.endHour || (currentHour === lastSlot.endHour && currentMin >= lastSlot.endMin))
: (selectedDate < now);
const missedPeriods = currentTimeSlots.filter(slot => {
if (slot.id === 'R') return false;
if (!isToday) {
// For past/future dates: treat as missed if no teacher log
return !teacherLogs[slot.id];
}
const hasEnded = currentHour > slot.endHour || (currentHour === slot.endHour && currentMin >= slot.endMin);
return hasEnded && !teacherLogs[slot.id];
});
const classesTeacherStatus = CLASSES.map(cls => {
const activeSlot = isToday ? getCurrentActiveSlot(cls, currentHour, currentMin) : null;
const clsLogs = allClassLogs[cls] || {};
const clsTeacherLogs = clsLogs.teacherLogs || {};
let hasTeacher = false;
let currentTeacherLog = null;
let isRecess = false;
if (activeSlot) {
if (activeSlot.id === 'R') {
isRecess = true;
} else if (clsTeacherLogs[activeSlot.id] && clsTeacherLogs[activeSlot.id].name) {
hasTeacher = true;
currentTeacherLog = clsTeacherLogs[activeSlot.id];
}
}
const cStudents = studentsData[cls] || [];
const cAbsents = hasAttendanceData ? (todaysAttendance[cls] || []) : [];
const cEnrolled = cStudents.length || 24;
const cAbsentCount = hasAttendanceData ? cAbsents.length : 0;
const cPresentCount = hasAttendanceData ? (cEnrolled - cAbsentCount) : 0;
const cPercentage = hasAttendanceData && cEnrolled > 0
? Math.round((cPresentCount / cEnrolled) * 100)
: 0;
return {
className: cls,
activeSlot,
hasTeacher,
currentTeacherLog,
isRecess,
enrolled: cEnrolled,
present: cPresentCount,
absent: cAbsentCount,
percentage: cPercentage,
absentees: cAbsents
};
});
const noTeacherClassesCount = classesTeacherStatus.filter(c => c.activeSlot && !c.isRecess && !c.hasTeacher).length;
const openTeacherModal = (slot) => {
if (slot.id === 'R') return;
setActiveSlot(slot);
setTempTeacher(teacherLogs[slot.id] || { subject: '', name: '' });
setIsTeacherModalOpen(true);
};
const saveTeacherLog = async () => {
if (!tempTeacher.name || isSaving || !activeSlot) return;
setIsSaving(true);
const slotId = activeSlot.id;
const teacher = { ...tempTeacher };
const cls = selectedClass;
const updatedTeacherLogs = { ...teacherLogs, [slotId]: teacher };
setAllClassLogs(prev => ({
...prev,
[cls]: { ...(prev[cls] || {}), teacherLogs: updatedTeacherLogs, studentLogs: (prev[cls] || {}).studentLogs || {} }
}));
setIsTeacherModalOpen(false);
await postLog({
className: cls,
type: 'teacher',
name: teacher.name,
period: slotId,
subject: teacher.subject,
action: 'set',
date: dateId
});
setIsSaving(false);
};
const openStudentModal = (slot) => {
setActiveSlot(slot);
setSelectedStudents([]);
setIsStudentModalOpen(true);
};
const saveStudentLog = async () => {
if (selectedStudents.length === 0 || isSaving || !activeSlot) return;
setIsSaving(true);
const slotId = activeSlot.id;
const students = [...selectedStudents];
const cls = selectedClass;
const logId = Date.now();
const timeOutStr = getMalaysiaTimeString();
const newLog = {
id: logId,
names: students,
timeOut: timeOutStr,
timeIn: null
};
const updatedSlotLogs = [...(studentLogs[slotId] || []), newLog];
const updatedStudentLogs = { ...studentLogs, [slotId]: updatedSlotLogs };
setAllClassLogs(prev => ({
...prev,
[cls]: { ...(prev[cls] || {}), teacherLogs: (prev[cls] || {}).teacherLogs || {}, studentLogs: updatedStudentLogs }
}));
setIsStudentModalOpen(false);
await postLog({
className: cls,
type: 'student',
name: '',
period: slotId,
subject: teacherLogs[slotId]?.subject || '',
action: 'exit',
students: students,
timeOut: timeOutStr,
timeOutDateTime: new Date().toLocaleString('en-CA', { timeZone: MALAYSIA_TIME_ZONE, hour12: false }),
timeZone: MALAYSIA_TIME_ZONE,
logId: logId,
date: dateId
});
setIsSaving(false);
};
const confirmDelete = async () => {
if (!itemToDelete || isSaving) return;
setIsSaving(true);
const toDelete = itemToDelete;
setItemToDelete(null);
if (toDelete.type === 'student') {
const { slotId, logId } = toDelete;
const updatedLogs = (studentLogs[slotId] || []).filter(log => log.id !== logId);
setAllClassLogs(prev => ({
...prev,
[selectedClass]: { ...selectedClassData, studentLogs: { ...studentLogs, [slotId]: updatedLogs } }
}));
await postLog({
className: selectedClass,
type: 'student',
period: slotId,
action: 'delete',
logId: logId,
date: dateId
});
} else if (toDelete.type === 'teacher') {
const { slotId } = toDelete;
const newTeacherLogs = { ...teacherLogs };
delete newTeacherLogs[slotId];
setAllClassLogs(prev => ({
...prev,
[selectedClass]: { ...selectedClassData, teacherLogs: newTeacherLogs }
}));
await postLog({
className: selectedClass,
type: 'teacher',
period: slotId,
action: 'delete',
date: dateId
});
}
setIsSaving(false);
};
const handleCopyPrevious = async (e, currentSlotId) => {
e.stopPropagation();
if (isSaving) return;
const currentIndex = currentTimeSlots.findIndex(s => s.id === currentSlotId);
let prevIndex = currentIndex - 1;
while (prevIndex >= 0 && (currentTimeSlots[prevIndex].id === 'R' || !teacherLogs[currentTimeSlots[prevIndex].id])) {
prevIndex--;
}
if (prevIndex >= 0) {
const prevLog = teacherLogs[currentTimeSlots[prevIndex].id];
if (prevLog && prevLog.name) {
setIsSaving(true);
const updatedTeacherLogs = { ...teacherLogs, [currentSlotId]: { ...prevLog } };
setAllClassLogs(prev => ({
...prev,
[selectedClass]: { ...selectedClassData, teacherLogs: updatedTeacherLogs }
}));
await postLog({
className: selectedClass,
type: 'teacher',
name: prevLog.name,
period: currentSlotId,
subject: prevLog.subject,
action: 'set',
date: dateId
});
setIsSaving(false);
}
}
};
return (
<div className="min-h-screen bg-slate-100 p-2 sm:p-4 md:p-8 text-slate-800 font-sans">
<div className="max-w-5xl mx-auto bg-white rounded-2xl shadow-xl overflow-hidden border border-slate-200">
<div className="bg-indigo-700 text-white p-4 md:p-6 pb-2 md:pb-4 flex flex-col gap-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-xl md:text-2xl font-bold flex items-center gap-2">
<BookOpen className="w-6 h-6 md:w-7 md:h-7" /> E-Buku Kawalan
</h1>
{isOnline ? (
<span className="bg-emerald-500/20 text-emerald-300 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1 border border-emerald-500/30">
<span className="w-1.5 h-1.5 bg-emerald-400 rounded-full animate-pulse"></span>
Disegerakkan Dalam Talian
</span>
) : (
<span className="bg-orange-500/20 text-orange-300 px-2 py-0.5 rounded-full text-[10px] font-bold flex items-center gap-1 border border-orange-500/30">
<CloudOff className="w-3 h-3" />
Mod Tempatan
</span>
)}
</div>
<p className="text-indigo-200 text-xs mt-1">
Sistem Pengurusan Kehadiran & Rekod Guru Seia Sekata | <span className="font-semibold text-white">Hak cipta SMK BANDAR KUCHING NO.1</span>
</p>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 text-sm">
<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">
<Calendar className="w-4 h-4 text-indigo-300 flex-shrink-0" />
<input
type="date"
value={selectedDateInputValue}
onChange={(e) => {
if (!e.target.value) return;
const [y, m, d] = e.target.value.split('-').map(Number);
const newDate = new Date(y, m - 1, d);
newDate.setHours(0, 0, 0, 0);
setSelectedDate(newDate);
}}
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]"
title="Pilih tarikh"
/>
<span className="text-indigo-200 text-xs hidden sm:inline whitespace-nowrap">
{selectedDate.toLocaleDateString('ms-MY', { weekday: 'long' })}
</span>
{!isToday && (
<button
type="button"
onClick={() => {
setSelectedDate(getMalaysiaDateObject());
}}
className="text-[10px] font-bold bg-white/20 hover:bg-white/30 px-1.5 py-0.5 rounded text-white whitespace-nowrap"
title="Kembali ke hari ini"
>
Hari Ini
</button>
)}
</div>
{activeTab === 'class_view' && (
<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">
<span className="font-semibold">Kelas:</span>
<select
value={selectedClass}
onChange={(e) => setSelectedClass(e.target.value)}
className="bg-indigo-900 border border-indigo-400/40 rounded px-2 py-1 text-white font-bold focus:outline-none cursor-pointer w-full sm:w-auto"
>
{CLASSES.map(cls => (
<option key={cls} value={cls}>{cls}</option>
))}
</select>
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 border-t border-indigo-600/50 pt-3 mt-1">
<button
onClick={() => setActiveTab('class_view')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs md:text-sm font-bold transition-all ${
activeTab === 'class_view'
? 'bg-white text-indigo-800 shadow-md'
: 'bg-indigo-800/50 text-indigo-200 hover:bg-indigo-800 hover:text-white'
}`}
>
<Users className="w-4 h-4" /> Buku Kawalan Kelas ({selectedClass})
</button>
<button
onClick={() => setActiveTab('school_summary')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs md:text-sm font-bold transition-all relative ${
activeTab === 'school_summary'
? 'bg-white text-indigo-800 shadow-md'
: 'bg-indigo-800/50 text-indigo-200 hover:bg-indigo-800 hover:text-white'
}`}
>
<LayoutDashboard className="w-4 h-4" /> Ringkasan Sekolah & Guru
{noTeacherClassesCount > 0 && (
<span className="bg-red-500 text-white text-[10px] px-1.5 py-0.2 rounded-full font-black animate-pulse">
{noTeacherClassesCount}
</span>
)}
</button>
</div>
</div>
{error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 m-4 md:m-6 mb-0 text-red-700 flex items-start gap-3 rounded-r-lg text-sm">
<AlertTriangle className="w-5 h-5 flex-shrink-0 text-red-500 mt-0.5" />
<div>
<p className="font-bold">Sambungan Gagal</p>
<p className="text-xs text-red-600 mt-0.5">{error}</p>
</div>
</div>
)}
{activeTab === 'school_summary' && (
<div className="p-4 md:p-6 bg-slate-50 min-h-[600px] space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Kadar Kehadiran Sekolah</div>
<div className="flex items-baseline gap-2">
<span className={`text-2xl md:text-3xl font-black ${schoolAttendancePercentage < 85 ? 'text-orange-500' : 'text-emerald-600'}`}>
{schoolAttendancePercentage}%
</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2 mt-2">
<div
className={`h-2 rounded-full ${schoolAttendancePercentage < 85 ? 'bg-orange-500' : 'bg-emerald-500'}`}
style={{ width: `${schoolAttendancePercentage}%` }}
></div>
</div>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider">Jumlah Enrolmen</div>
<div className="text-2xl font-black text-slate-800 mt-1">{schoolTotalEnrolled}</div>
<div className="text-[11px] text-slate-400">{CLASSES.length} Kelas</div>
</div>
<div className="w-10 h-10 rounded-full bg-indigo-50 flex items-center justify-center text-indigo-600">
<Users className="w-5 h-5" />
</div>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider">Pelajar Hadir</div>
<div className="text-2xl font-black text-emerald-600 mt-1">{schoolTotalPresent}</div>
<div className="text-[11px] text-emerald-600 font-semibold">Ada di Sekolah</div>
</div>
<div className="w-10 h-10 rounded-full bg-emerald-50 flex items-center justify-center text-emerald-600">
<UserCheck className="w-5 h-5" />
</div>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center justify-between">
<div>
<div className="text-xs font-bold text-slate-500 uppercase tracking-wider">Pelajar Tidak Hadir</div>
<div className="text-2xl font-black text-red-600 mt-1">{schoolTotalAbsent}</div>
<div className="text-[11px] text-red-500 font-semibold">Seluruh Sekolah</div>
</div>
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center text-red-600">
<UserX className="w-5 h-5" />
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-4 md:p-6 space-y-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 border-b border-slate-100 pb-4">
<div>
<h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
<Activity className="w-5 h-5 text-indigo-600" /> Status Guru {isToday ? 'Semasa (Real-Time)' : 'Mengikut Tarikh Dipilih'}
</h2>
<p className="text-xs text-slate-500 mt-0.5">
{isToday ? (
<>
Memaparkan status guru mengajar di setiap kelas mengikut waktu jam sekarang (
<span className="font-bold text-indigo-700">
{now.toLocaleTimeString('ms-MY', { hour: '2-digit', minute: '2-digit' })}
</span>)
</>
) : (
<>
Memaparkan rekod guru untuk tarikh{' '}
<span className="font-bold text-indigo-700">
{selectedDate.toLocaleDateString('ms-MY', { weekday: 'long', day: 'numeric', month: 'short', year: 'numeric' })}
</span>
{' '}(bukan hari ini)
</>
)}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => setTeacherFilter('all')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-colors ${
teacherFilter === 'all'
? 'bg-slate-800 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
Semua Kelas ({CLASSES.length})
</button>
<button
onClick={() => setTeacherFilter('no_teacher')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-colors ${
teacherFilter === 'no_teacher'
? 'bg-red-600 text-white'
: 'bg-red-50 text-red-700 border border-red-200 hover:bg-red-100'
}`}
>
<ShieldAlert className="w-3.5 h-3.5" /> Tiada Guru ({noTeacherClassesCount})
</button>
<button
onClick={() => setTeacherFilter('has_teacher')}
className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-colors ${
teacherFilter === 'has_teacher'
? 'bg-emerald-600 text-white'
: 'bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100'
}`}
>
<UserCheck className="w-3.5 h-3.5" /> Ada Guru ({CLASSES.length - noTeacherClassesCount})
</button>
</div>
</div>
{noTeacherClassesCount > 0 ? (
<div className="bg-red-50 border border-red-200 text-red-800 p-3.5 rounded-xl flex items-center justify-between gap-3 text-xs md:text-sm font-semibold">
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 bg-red-600 rounded-full animate-ping"></span>
<span>Terdapat <strong>{noTeacherClassesCount} kelas</strong> yang belum mendaftar guru pada waktu ini!</span>
</div>
<button
onClick={() => setTeacherFilter('no_teacher')}
className="underline text-red-700 hover:text-red-900 font-bold whitespace-nowrap"
>
Tunjuk Kelas
</button>
</div>
) : (
<div className="bg-emerald-50 border border-emerald-200 text-emerald-800 p-3 rounded-xl flex items-center gap-2 text-xs font-semibold">
<UserCheck className="w-4 h-4 text-emerald-600" />
<span>Semua kelas beroperasi dengan guru bertugas pada waktu sekarang.</span>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 pt-2">
{classesTeacherStatus
.filter(cStatus => {
if (searchQuery && !cStatus.className.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
if (teacherFilter === 'no_teacher') {
return cStatus.activeSlot && !cStatus.isRecess && !cStatus.hasTeacher;
}
if (teacherFilter === 'has_teacher') {
return cStatus.isRecess || cStatus.hasTeacher;
}
return true;
})
.map(cStatus => {
const isNoTeacher = cStatus.activeSlot && !cStatus.isRecess && !cStatus.hasTeacher;
return (
<div
key={cStatus.className}
className={`p-4 rounded-xl border transition-all ${
isNoTeacher
? 'bg-red-50/70 border-red-200 shadow-sm'
: cStatus.isRecess
? 'bg-amber-50/60 border-amber-200'
: cStatus.hasTeacher
? 'bg-emerald-50/50 border-emerald-200'
: 'bg-slate-50 border-slate-200'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="font-black text-slate-800 text-lg flex items-center gap-1.5">
Kelas {cStatus.className}
</span>
{isNoTeacher ? (
<span className="bg-red-600 text-white text-[10px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider flex items-center gap-1 animate-pulse">
<ShieldAlert className="w-3 h-3" /> Tiada Guru
</span>
) : cStatus.isRecess ? (
<span className="bg-amber-500 text-white text-[10px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider">
Rehat
</span>
) : cStatus.hasTeacher ? (
<span className="bg-emerald-600 text-white text-[10px] font-bold px-2 py-0.5 rounded-full uppercase tracking-wider flex items-center gap-1">
<UserCheck className="w-3 h-3" /> Ada Guru
</span>
) : (
<span className="bg-slate-200 text-slate-600 text-[10px] font-bold px-2 py-0.5 rounded-full">
Luar Waktu
</span>
)}
</div>
<div className="text-xs text-slate-500 mb-2">
{cStatus.activeSlot ? (
<span>Waktu {cStatus.activeSlot.label} ({cStatus.activeSlot.time})</span>
) : (
<span className="italic text-slate-400">Luar waktu persekolahan</span>
)}
</div>
<div className="min-h-[44px] flex flex-col justify-center bg-white p-2.5 rounded-lg border border-slate-100">
{cStatus.hasTeacher && cStatus.currentTeacherLog ? (
<div>
<div className="text-xs font-bold text-indigo-700 truncate">
{cStatus.currentTeacherLog.name}
</div>
<div className="text-[11px] text-slate-500 font-medium">
Subjek: {cStatus.currentTeacherLog.subject || '-'}
</div>
</div>
) : cStatus.isRecess ? (
<div className="text-xs font-semibold text-amber-600 italic">
Pelajar sedang rehat
</div>
) : isNoTeacher ? (
<div className="text-xs font-bold text-red-600 flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5 text-red-500 flex-shrink-0" />
<span>Belum mendaftar masuk</span>
</div>
) : (
<div className="text-xs text-slate-400 italic">
Tiada maklumat
</div>
)}
</div>
<button
onClick={() => {
setSelectedClass(cStatus.className);
setActiveTab('class_view');
}}
className="w-full mt-3 py-1.5 px-3 bg-white hover:bg-slate-100 text-indigo-600 border border-indigo-100 rounded-lg text-xs font-bold flex items-center justify-center gap-1 transition-colors"
>
Pergi Ke Kelas <ArrowRight className="w-3.5 h-3.5" />
</button>
</div>
);
})}
</div>
</div>
</div>
)}
{activeTab === 'class_view' && (
<>
<div className="p-4 md:p-6 grid grid-cols-1 lg:grid-cols-2 gap-6 border-b border-slate-100 bg-white">
<div className="space-y-3">
<h2 className="text-xs font-bold uppercase tracking-wider text-slate-500">
Kehadiran {isToday ? 'Hari Ini' : 'Tarikh Dipilih'} ({selectedClass})
</h2>
<div className="grid grid-cols-2 gap-3">
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 flex items-center justify-between">
<span className="text-xs md:text-sm font-medium text-slate-600">Hadir:</span>
<span className="text-lg md:text-xl font-black text-emerald-600">{presentCount}</span>
</div>
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 flex items-center justify-between">
<span className="text-xs md:text-sm font-medium text-slate-600">Tidak Hadir:</span>
<span className="text-lg md:text-xl font-black text-red-600">{absentCount}</span>
</div>
</div>
<div className="bg-slate-50 p-3 rounded-xl border border-slate-200 flex items-center justify-between">
<span className="text-xs md:text-sm font-medium text-slate-600">Jumlah Daftar:</span>
<span className="text-base md:text-lg font-bold text-slate-800">{totalStudents}</span>
</div>
{!hasAttendanceData && (
<p className="text-[11px] text-amber-600 font-medium italic">Tiada data kehadiran untuk tarikh ini</p>
)}
</div>
<div>
<h2 className="text-xs font-bold uppercase tracking-wider text-slate-500 mb-3">Senarai Tidak Hadir</h2>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 min-h-[120px] flex flex-col justify-center">
{isLoading ? (
<div className="text-center text-slate-400 text-sm italic">Memuatkan data...</div>
) : !hasAttendanceData ? (
<div className="flex flex-col items-center justify-center text-slate-400 italic">
<span className="text-sm">Tiada data kehadiran</span>
<span className="text-[11px] mt-1">(contoh: hari cuti / hujung minggu)</span>
</div>
) : absenteesForClass && absenteesForClass.length > 0 ? (
<ul className="text-sm space-y-1.5 max-h-32 overflow-y-auto pr-1">
{absenteesForClass.map((student, idx) => (
<li key={idx} className="text-red-600 font-medium flex items-center text-xs">
<span className="w-1.5 h-1.5 bg-red-500 rounded-full mr-2 flex-shrink-0"></span>
{student}
</li>
))}
</ul>
) : (
<div className="flex flex-col items-center justify-center text-slate-400 italic">
<span className="text-sm">Semua pelajar hadir</span>
</div>
)}
</div>
</div>
</div>
<div className="p-4 md:p-6 bg-slate-50 border-b border-slate-200">
<h2 className="text-xs font-bold uppercase tracking-wider text-slate-500 mb-4 flex items-center gap-2">
<PieChart className="w-4 h-4" /> Analisis Semasa Kelas {selectedClass}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div className="text-xs font-bold text-slate-500 mb-1">
Kadar Kehadiran {hasAttendanceData ? `(${presentCount}/${totalStudents})` : ''}
</div>
<div className="flex items-baseline gap-2 mb-2">
<span className={`text-2xl md:text-3xl font-black ${
!hasAttendanceData ? 'text-slate-400' : attendancePercentage < 80 ? 'text-orange-500' : 'text-emerald-500'
}`}>
{hasAttendanceData ? `${attendancePercentage}%` : '—'}
</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2.5">
<div
className={`h-2.5 rounded-full ${
!hasAttendanceData ? 'bg-slate-300' : attendancePercentage < 80 ? 'bg-orange-500' : 'bg-emerald-500'
}`}
style={{ width: `${hasAttendanceData ? attendancePercentage : 0}%` }}
></div>
</div>
{!hasAttendanceData && (
<p className="text-[10px] text-slate-400 italic mt-1">Tiada data</p>
)}
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<div className="text-xs font-bold text-slate-500 mb-3 flex items-center gap-1">
<Activity className="w-3.5 h-3.5" /> Kerap Keluar Kelas
</div>
{frequentExits.length > 0 ? (
<ul className="text-[11px] space-y-2">
{frequentExits.map(([name, count]) => (
<li key={name} className="flex justify-between items-center border-b border-slate-50 pb-1">
<span className="truncate font-medium text-slate-700 pr-2">{name}</span>
<span className="bg-orange-100 text-orange-700 px-2 py-0.5 rounded-full font-bold whitespace-nowrap">
{count} kali
</span>
</li>
))}
</ul>
) : (
<div className="text-[11px] text-slate-400 italic mt-2">Tiada pergerakan.</div>
)}
</div>
<div className={`p-4 rounded-xl border shadow-sm sm:col-span-2 md:col-span-1 ${missedPeriods.length > 0 ? 'bg-rose-50 border-rose-200' : 'bg-emerald-50 border-emerald-200'}`}>
<div className={`text-xs font-bold mb-2 flex items-center gap-1 ${missedPeriods.length > 0 ? 'text-rose-700' : 'text-emerald-700'}`}>
<Clock className="w-3.5 h-3.5"/> Log Belum Diisi
{missedPeriods.length > 0 && (
<span className="ml-auto bg-rose-600 text-white text-[10px] px-1.5 py-0.5 rounded-full font-black">{missedPeriods.length}</span>
)}
</div>
{missedPeriods.length === 0 ? (
<span className="text-[10px] text-emerald-600 font-bold bg-emerald-100 px-2 py-0.5 rounded">Semua log lengkap ✓</span>
) : isAfterSchool && missedPeriods.length > 6 ? (
<div className="space-y-2">
<p className="text-[11px] text-rose-600 font-medium">
{missedPeriods.length} waktu belum dilog hari ini
</p>
<div className="flex flex-wrap gap-1">
{missedPeriods.map(p => (
<button
key={p.id}
onClick={() => openTeacherModal(p)}
className="px-2 py-0.5 bg-white border border-rose-200 rounded text-rose-600 text-[10px] font-bold hover:bg-rose-100 hover:border-rose-400 transition-colors cursor-pointer"
title="Klik untuk set guru"
>
{p.label}
</button>
))}
</div>
</div>
) : (
<div className="flex flex-wrap gap-1.5">
{missedPeriods.map(p => (
<button
key={p.id}
onClick={() => openTeacherModal(p)}
className="px-2 py-0.5 bg-white border border-rose-200 rounded text-rose-600 text-[10px] font-bold hover:bg-rose-100 hover:border-rose-400 transition-colors cursor-pointer"
title="Klik untuk set guru"
>
Waktu {p.label}
</button>
))}
</div>
)}
</div>
</div>
</div>
<div className="p-4 md:p-6 bg-slate-50/30">
<div className="hidden md:grid grid-cols-[100px_100px_220px_1fr] gap-4 p-3 border-b border-slate-200 text-xs font-bold uppercase tracking-wider text-slate-500 bg-slate-50 rounded-t-lg">
<div>Waktu</div>
<div>Mata Pelajaran</div>
<div>Nama Guru</div>
<div>Pelajar di luar kelas</div>
</div>
<div className="space-y-4 md:space-y-0 md:divide-y md:divide-slate-200">
{currentTimeSlots.map((slot) => {
const isRecess = slot.id === 'R';
const tLog = teacherLogs[slot.id];
const sLogs = studentLogs[slot.id] || [];
const isCurrentActiveSlot = slot.id === activeTimeSlotId;
return (
<div key={slot.id} className={`bg-white md:bg-transparent rounded-xl border shadow-sm md:shadow-none p-4 md:p-3 md:grid md:grid-cols-[100px_100px_220px_1fr] md:gap-4 md:items-center transition-all ${
isCurrentActiveSlot
? 'border-2 border-indigo-500 md:border-2 md:border-indigo-500 bg-indigo-100 md:bg-indigo-100 shadow-md ring-2 ring-indigo-300/50 z-10 relative'
: isRecess
? 'border-slate-200 md:border-transparent bg-orange-50 md:bg-orange-50/50'
: 'border-slate-200 md:border-transparent hover:bg-slate-50'
}`}>
<div className="flex justify-between items-center md:block mb-3 md:mb-0 border-b border-slate-100 md:border-0 pb-3 md:pb-0">
<span className="md:hidden text-xs font-bold text-slate-400 uppercase">Waktu</span>
<div>
<div className="flex items-center gap-2">
<div className={`font-bold text-lg md:text-sm ${isRecess ? 'text-orange-700' : (isCurrentActiveSlot ? 'text-indigo-800' : 'text-slate-800')}`}>
{slot.label}
</div>
{isCurrentActiveSlot && (
<span className="bg-indigo-600 text-white text-[9px] px-1.5 py-0.5 rounded uppercase font-black tracking-wider animate-pulse shadow-sm">
Semasa
</span>
)}
</div>
<div className={`text-xs ${isRecess ? 'text-orange-500' : (isCurrentActiveSlot ? 'text-indigo-600 font-semibold' : 'text-slate-500')}`}>
{slot.time}
</div>
</div>
</div>
<div className="flex justify-between items-center md:block mb-3 md:mb-0">
<span className="md:hidden text-xs font-bold text-slate-400 uppercase">Subjek</span>
<div className="font-medium text-slate-700 md:text-sm">
{isRecess ? '-' : (tLog ? tLog.subject : '-')}
</div>
</div>
<div className="flex justify-between items-center md:block mb-3 md:mb-0">
<span className="md:hidden text-xs font-bold text-slate-400 uppercase">Guru</span>
{isRecess ? (
<span className="text-orange-600 font-medium text-sm italic">Waktu Rehat</span>
) : (
<div className="flex items-center justify-end md:justify-start gap-1 group">
{tLog && tLog.name ? (
<>
<button onClick={() => openTeacherModal(slot)} className="text-right md:text-left">
<span className="font-bold text-indigo-700 text-sm bg-indigo-50 md:bg-transparent px-2 py-1 md:p-0 rounded-lg hover:text-indigo-900 transition-colors">
{tLog.name}
</span>
</button>
<button
onClick={(e) => handleCopyPrevious(e, slot.id)}
className="text-indigo-400 hover:text-indigo-600 p-1 transition-colors ml-1"
title="Salin rekod sebelumnya"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={(e) => { e.stopPropagation(); setItemToDelete({ type: 'teacher', slotId: slot.id }); }}
className="text-red-400 hover:text-red-600 p-1 md:opacity-0 group-hover:opacity-100 transition-opacity"
title="Padam rekod guru"
>
<Trash2 className="w-4 h-4" />
</button>
</>
) : (
<>
<button onClick={() => openTeacherModal(slot)} className="text-right md:text-left">
<span className="text-indigo-600 hover:text-indigo-800 font-semibold text-xs flex items-center justify-end md:justify-start gap-1 bg-indigo-50 md:bg-transparent px-3 py-1.5 md:p-0 rounded-lg">
<Users className="w-3.5 h-3.5" /> Set Guru
</span>
</button>
<button
onClick={(e) => handleCopyPrevious(e, slot.id)}
className="text-indigo-400 hover:text-indigo-600 p-1 transition-colors ml-1"
title="Salin rekod sebelumnya"
>
<Copy className="w-4 h-4" />
</button>
</>
)}
</div>
)}
</div>
<div className="mt-3 pt-3 border-t border-slate-100 md:border-0 md:mt-0 md:pt-0">
<span className="md:hidden text-xs font-bold text-slate-400 uppercase mb-3 block">Pelajar Keluar</span>
<div className="flex flex-col gap-2">
{sLogs.map(log => (
<div key={log.id} className="flex flex-wrap items-center justify-between bg-slate-100/80 p-2.5 rounded-lg text-xs group border border-slate-200/60">
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span className="font-bold text-slate-700">{log.names.join(', ')}</span>
<span className="bg-orange-100 text-orange-700 px-1.5 py-0.5 rounded text-[10px] font-bold w-max">
Keluar: {String(log.timeOut || '').match(/\d{1,2}:\d{2}/)?.[0] || log.timeOut}
</span>
</div>
<button
onClick={() => setItemToDelete({ type: 'student', slotId: slot.id, logId: log.id })}
className="text-red-400 hover:text-red-600 p-1 md:opacity-0 group-hover:opacity-100 transition-opacity"
title="Padam rekod"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
{!isRecess && (
<button onClick={() => openStudentModal(slot)} className="text-indigo-500 hover:text-indigo-700 bg-indigo-50/50 md:bg-transparent border border-indigo-100 md:border-0 rounded-lg p-2 md:p-0 font-medium text-xs text-center md:text-left w-full md:w-max mt-1">
+ Tambah pelajar keluar...
</button>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</>
)}
</div>
{isTeacherModalOpen && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-4 z-50">
<div className="bg-white rounded-t-2xl sm:rounded-2xl p-6 w-full max-w-sm">
<h3 className="font-bold text-lg mb-4">Set Guru: Waktu {activeSlot?.label}</h3>
<label className="block text-xs font-bold text-slate-500 mb-1">Mata Pelajaran</label>
<select
value={tempTeacher.subject}
onChange={e => setTempTeacher({...tempTeacher, subject: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 mb-4 text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
>
<option value="">-- Pilih --</option>
{subjectsData.map(sub => <option key={sub} value={sub}>{sub}</option>)}
</select>
<label className="block text-xs font-bold text-slate-500 mb-1">Nama Guru</label>
<select
value={tempTeacher.name}
onChange={e => setTempTeacher({...tempTeacher, name: e.target.value})}
className="w-full border border-slate-300 rounded-lg p-3 mb-6 text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
>
<option value="">-- Pilih --</option>
{teachersData.map(t => <option key={t} value={t}>{t}</option>)}
</select>
<div className="flex gap-3">
<button onClick={() => setIsTeacherModalOpen(false)} disabled={isSaving} className="flex-1 py-3 text-sm font-bold text-slate-600 bg-slate-100 rounded-xl hover:bg-slate-200 disabled:opacity-50">Batal</button>
<button onClick={saveTeacherLog} disabled={isSaving || !tempTeacher.name} className="flex-1 py-3 text-sm font-bold text-white bg-indigo-600 rounded-xl hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed">
{isSaving ? 'Menyimpan...' : 'Simpan'}
</button>
</div>
</div>
</div>
)}
{isStudentModalOpen && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-4 z-50">
<div className="bg-white rounded-t-2xl sm:rounded-2xl p-6 w-full max-w-md h-[85vh] sm:h-auto sm:max-h-[85vh] flex flex-col">
<h3 className="font-bold text-lg mb-1">Rekod Keluar: Waktu {activeSlot?.label}</h3>
<p className="text-[11px] text-slate-500 mb-4">Pelajar tidak hadir telah dikelabukan.</p>
<div className="overflow-y-auto flex-1 border border-slate-200 rounded-xl mb-4">
{classStudents.map(student => {
const isAbsent = absenteesForClass?.includes(student);
const isSelected = selectedStudents.includes(student);
return (
<div
key={student}
onClick={() => {
if (isAbsent) return;
setSelectedStudents(prev =>
isSelected ? prev.filter(s => s !== student) : [...prev, student]
);
}}
className={`p-3.5 border-b border-slate-100 last:border-0 text-sm cursor-pointer flex items-center gap-3 transition-colors
${isAbsent ? 'opacity-40 cursor-not-allowed bg-slate-50' : 'hover:bg-slate-50 active:bg-slate-100'}
`}
>
<div className={`w-5 h-5 rounded border flex items-center justify-center flex-shrink-0 transition-colors
${isSelected ? 'bg-indigo-600 border-indigo-600' : 'border-slate-300'}
`}>
{isSelected && <CheckSquare className="w-3.5 h-3.5 text-white" />}
</div>
<span className={isSelected ? 'font-bold text-indigo-700' : 'text-slate-700'}>
{student} {isAbsent && <span className="text-xs font-normal italic text-slate-500">(Tidak Hadir)</span>}
</span>
</div>
);
})}
</div>
<div className="flex gap-3 mt-auto pt-2">
<button onClick={() => setIsStudentModalOpen(false)} className="flex-1 py-3 text-sm font-bold text-slate-600 bg-slate-100 rounded-xl hover:bg-slate-200">Batal</button>
<button onClick={saveStudentLog} disabled={selectedStudents.length === 0 || isSaving} className="flex-1 py-3 text-sm font-bold text-white bg-indigo-600 rounded-xl hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed">
{isSaving ? 'Menyimpan...' : 'Rekod Keluar'}
</button>
</div>
</div>
</div>
)}
{itemToDelete && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-2xl p-6 w-full max-w-sm text-center">
<div className="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
<Trash2 className="w-6 h-6 text-red-600" />
</div>
<h3 className="font-bold text-lg mb-2 text-slate-800">Padam Rekod?</h3>
<p className="text-sm text-slate-500 mb-6">
{itemToDelete.type === 'teacher'
? "Adakah anda pasti untuk memadam rekod guru ini?"
: "Adakah anda pasti untuk memadam rekod pelajar keluar ini?"}
</p>
<div className="flex gap-3">
<button onClick={() => setItemToDelete(null)} className="flex-1 py-3 text-sm font-bold text-slate-600 bg-slate-100 rounded-xl hover:bg-slate-200">Batal</button>
<button onClick={confirmDelete} className="flex-1 py-3 text-sm font-bold text-white bg-red-600 rounded-xl hover:bg-red-700">Ya, Padam</button>
</div>
</div>
</div>
)}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
<footer className="text-center py-4 bg-slate-200 text-slate-600 text-xs font-semibold">
Hak Cipta &copy; SMK BANDAR KUCHING NO.1. Hak Cipta Terpelihara.
</footer>
</body>
</html>