SAT@NPS

Loading user...

Loading student data...

Weekly Absence Summary

No absences recorded for this week.
// Global variables provided by the environment const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; let db; let auth; let userId = null; let selectedStudent = null; let chatHistory = []; let isTyping = false; let currentWeekStart = new Date(); // Adjust the start date to be the most recent Monday const today = new Date(); const dayOfWeek = today.getDay(); const diff = today.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); currentWeekStart.setDate(diff); currentWeekStart.setHours(0, 0, 0, 0); let students = []; // Global students array for easy access // DOM elements const studentListEl = document.getElementById('student-list'); const userIdDisplayEl = document.getElementById('user-id-display'); const popupContainerEl = document.getElementById('popup-container'); const popupContentEl = document.getElementById('popup-content'); const chatbotModalEl = document.getElementById('chatbot-modal'); const chatTitleEl = document.getElementById('chat-title'); const chatHistoryEl = document.getElementById('chat-history'); const chatFormEl = document.getElementById('chat-form'); const chatInputEl = document.getElementById('chat-input'); const chatSubmitBtnEl = document.getElementById('chat-submit-btn'); const closeChatBtnEl = document.getElementById('close-chat-btn'); const weekDisplayEl = document.getElementById('week-display'); const prevWeekBtn = document.getElementById('prev-week-btn'); const nextWeekBtn = document.getElementById('next-week-btn'); const absenceSummaryEl = document.getElementById('absence-summary'); // --- Firebase Initialization --- const initFirebase = async () => { try { // Use the user-provided config if it's available, otherwise fallback to the canvas environment variable. const appConfig = (Object.keys(firebaseConfig).length > 0) ? firebaseConfig : JSON.parse(__firebase_config); const app = initializeApp(appConfig); auth = getAuth(app); db = getFirestore(app); onAuthStateChanged(auth, async (user) => { if (user) { userId = user.uid; } else if (initialAuthToken) { await signInWithCustomToken(auth, initialAuthToken); } else { await signInAnonymously(auth); } userIdDisplayEl.textContent = `User: ${userId}`; setupRealtimeListener(); renderWeekDisplay(); }); } catch (error) { console.error("Firebase initialization failed:", error); showTemporaryPopup("Error initializing Firebase. Please check your configuration."); } }; // --- Utility Functions --- const formatDate = (date) => { return date.toLocaleDateString('en-SG', { day: 'numeric', month: 'short' }); }; const isHolidayWeek = (date) => { const holidayStart = new Date('2025-09-08'); const holidayEnd = new Date('2025-09-12'); const weekStart = new Date(date); weekStart.setDate(weekStart.getDate() - weekStart.getDay() + (weekStart.getDay() === 0 ? -6 : 1)); const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 4); return weekStart.getTime() >= holidayStart.getTime() && weekEnd.getTime() <= holidayEnd.getTime(); } const getWeekdays = (startDate) => { const weekdays = []; for (let i = 0; i < 5; i++) { const day = new Date(startDate); day.setDate(startDate.getDate() + i); weekdays.push(day); } return weekdays; }; const renderWeekDisplay = () => { const endDate = new Date(currentWeekStart); endDate.setDate(currentWeekStart.getDate() + 4); weekDisplayEl.textContent = `${formatDate(currentWeekStart)} - ${formatDate(endDate)}`; }; const goToPreviousWeek = () => { currentWeekStart.setDate(currentWeekStart.getDate() - 7); renderWeekDisplay(); }; const goToNextWeek = () => { currentWeekStart.setDate(currentWeekStart.getDate() + 7); renderWeekDisplay(); }; // --- Data Management Functions --- const showTemporaryPopup = (message) => { popupContentEl.textContent = message; popupContainerEl.classList.remove('opacity-0'); popupContainerEl.classList.add('opacity-100'); setTimeout(() => { popupContainerEl.classList.remove('opacity-100'); popupContainerEl.classList.add('opacity-0'); }, 3000); }; const logAttendance = async (studentId, date, status) => { if (!db) { showTemporaryPopup("Database not ready."); return; } if (isHolidayWeek(new Date(date))) { showTemporaryPopup("Cannot log attendance for a school holiday."); return; } try { const studentRef = doc(db, `artifacts/${appId}/public/data/students`, studentId); const studentData = students.find(s => s.id === studentId); if (!studentData) { console.error("Student document does not exist."); showTemporaryPopup("Error: Student record not found."); return; } const existingEntryIndex = studentData.history.findIndex(h => h.date === date); let updatedHistory = [...studentData.history]; if (existingEntryIndex > -1) { updatedHistory[existingEntryIndex] = { date, status }; } else { updatedHistory.push({ date, status }); } await updateDoc(studentRef, { history: updatedHistory }); showTemporaryPopup(`Attendance for ${studentData.name} marked as ${status}.`); } catch (e) { console.error("Error updating attendance:", e); showTemporaryPopup("Failed to update attendance."); } }; const hasConsecutiveAbsence = (student) => { // Filter out holiday absences from the history const absentRecords = student.history.filter(h => h.status === 'absent'); if (absentRecords.length < 3) return false; const sortedAbsences = absentRecords.sort((a, b) => new Date(b.date) - new Date(a.date)); if (sortedAbsences.length >= 3) { const day1 = new Date(sortedAbsences[0].date); const day2 = new Date(sortedAbsences[1].date); const day3 = new Date(sortedAbsences[2].date); const isWeekday = (date) => { const day = date.getDay(); return day >= 1 && day <= 5; }; const checkConsecutiveWeekdays = (date1, date2) => { let nextDay = new Date(date1); nextDay.setDate(nextDay.getDate() - 1); while (!isWeekday(nextDay)) { nextDay.setDate(nextDay.getDate() - 1); } return nextDay.getTime() === date2.getTime(); }; return checkConsecutiveWeekdays(day1, day2) && checkConsecutiveWeekdays(day2, day3); } return false; }; const getAttendanceStatus = (student, date) => { if (isHolidayWeek(new Date(date))) { return 'SH'; } const record = student.history.find(h => h.date === date); return record ? record.status : 'undetermined'; }; const renderStudents = (students) => { const groupedStudents = students.reduce((acc, student) => { const level = student.level; if (!acc[level]) { acc[level] = []; } acc[level].push(student); return acc; }, {}); const weekdays = getWeekdays(currentWeekStart); const isCurrentWeekHoliday = isHolidayWeek(currentWeekStart); let html = ''; const sortedLevels = Object.keys(groupedStudents).sort(); sortedLevels.forEach(level => { html += `

Primary ${level.replace('P', '')}

`; groupedStudents[level].forEach(student => { const isAbsent = hasConsecutiveAbsence(student); html += `

${student.name}

${isAbsent && !isCurrentWeekHoliday ? `

Consecutive Absences - Propose Home Visit

` : ''}
${weekdays.map(day => { const dateStr = day.toISOString().slice(0, 10); let status = getAttendanceStatus(student, dateStr); const dayName = day.toLocaleDateString('en-US', { weekday: 'short' }); let buttonPresentClass = 'bg-white text-green-600 border border-green-300 hover:bg-green-50 hover:text-green-700'; let buttonAbsentClass = 'bg-white text-red-600 border border-red-300 hover:bg-red-50 hover:text-red-700'; let buttonSHClass = 'bg-white text-blue-600 border border-blue-300 hover:bg-blue-50 hover:text-blue-700'; if (status === 'present') buttonPresentClass = 'bg-green-500 text-white shadow-lg transform scale-105'; if (status === 'absent') buttonAbsentClass = 'bg-red-500 text-white shadow-lg transform scale-105'; if (status === 'SH') buttonSHClass = 'bg-blue-500 text-white shadow-lg transform scale-105'; return `
${dayName}
`; }).join('')} ${isAbsent && !isCurrentWeekHoliday ? ` ` : ''}
`; }); html += `
`; }); studentListEl.innerHTML = html; }; const renderAbsenceSummary = (students) => { const weekdays = getWeekdays(currentWeekStart); let html = ''; let hasAbsences = false; const isCurrentWeekHoliday = isHolidayWeek(currentWeekStart); weekdays.forEach(day => { const dateStr = day.toISOString().slice(0, 10); let absentStudents = []; if (isCurrentWeekHoliday) { absentStudents = students; } else { absentStudents = students.filter(student => student.history.some(h => h.date === dateStr && h.status === 'absent') ); } if (absentStudents.length > 0) { hasAbsences = true; html += `

${day.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}

`; } }); if (!hasAbsences) { html = `
No absences recorded for this week.
`; } absenceSummaryEl.innerHTML = html; }; const setupRealtimeListener = () => { if (!db) return; const studentsCol = collection(db, `artifacts/${appId}/public/data/students`); const checkAndPopulate = async () => { const q = query(studentsCol); const querySnapshot = await getDocs(q); if (querySnapshot.empty) { const initialStudentData = [ { name: 'Sayyidah', level: 'P1', history: [] }, { name: 'Qhalisha', level: 'P1', history: [] }, { name: 'Mariyah', level: 'P1', history: [] }, { name: 'Shaina', level: 'P3', history: [] }, { name: 'Zeryna', level: 'P3', history: [] }, { name: 'Aryan', level: 'P5', history: [] }, { name: 'Bella', level: 'P5', history: [] }, { name: 'Moosa', level: 'P5', history: [] }, { name: 'Rayyan', level: 'P5', history: [] }, { name: 'Umairah', level: 'P6', history: [] }, { name: 'Qhalifa', level: 'P6', history: [] }, ]; for (const student of initialStudentData) { await setDoc(doc(studentsCol, student.name), student); } } }; checkAndPopulate(); onSnapshot(studentsCol, (snapshot) => { students = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); renderStudents(students); renderAbsenceSummary(students); }); }; // --- Chatbot Logic --- const openChat = (student) => { selectedStudent = student; chatTitleEl.textContent = `Approach for Home Visit for ${student.name}`; chatHistoryEl.innerHTML = `
Hello! I'm here to help you prepare for a home visit for ${student.name}. How can I assist you?
`; chatbotModalEl.classList.remove('hidden'); }; const closeChat = () => { chatbotModalEl.classList.add('hidden'); selectedStudent = null; chatHistory = []; }; const displayChatMessages = (messages) => { chatHistoryEl.innerHTML = ''; messages.forEach(msg => { const messageEl = document.createElement('div'); messageEl.classList.add('flex', msg.role === 'user' ? 'justify-end' : 'justify-start'); let messageContent = msg.content; if (msg.isSuggestion) { messageContent = `
Home Visit Suggestions:
`; } messageEl.innerHTML = `
${messageContent}
`; chatHistoryEl.appendChild(messageEl); }); chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight; }; const showTypingIndicator = () => { isTyping = true; const typingEl = document.createElement('div'); typingEl.id = 'typing-indicator'; typingEl.classList.add('flex', 'justify-start'); typingEl.innerHTML = `
`; chatHistoryEl.appendChild(typingEl); chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight; }; const hideTypingIndicator = () => { isTyping = false; const typingEl = document.getElementById('typing-indicator'); if (typingEl) { typingEl.remove(); } }; const handleChatSubmit = async (e) => { e.preventDefault(); const message = chatInputEl.value.trim(); if (!message || !selectedStudent || isTyping) return; chatInputEl.value = ''; chatHistory.push({ role: 'user', content: message }); displayChatMessages(chatHistory); showTypingIndicator(); const systemPrompt = "You are a compassionate and helpful school administrator. Your role is to assist Year Heads and Form Teachers in understanding why a student might be absent and to provide empathetic, professional suggestions for a home visit. The suggestions should focus on building trust and offering support, not on being accusatory. Provide your response as a JSON object with a key 'reasons' for potential reasons, and a key 'suggestions' with an array of the 3 best bullet points. Do not include any other text besides the JSON."; const userQuery = `The student ${selectedStudent.name} (level: ${selectedStudent.level}) has been absent for 3 consecutive days. What are some potential reasons for their absence, and what are the 3 best, most empathetic, and actionable suggestions for the Form Teacher to use during a home visit?`; const apiKey = ""; const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`; const payload = { contents: [{ parts: [{ text: userQuery }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { "reasons": { "type": "STRING" }, "suggestions": { "type": "ARRAY", "items": { "type": "STRING" }, "maxItems": 3 } } } } }; try { const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); const jsonText = result?.candidates?.[0]?.content?.parts?.[0]?.text; const parsedData = JSON.parse(jsonText); let responseHtml = `

Potential Reasons:

${parsedData.reasons}

`; if (parsedData.suggestions && parsedData.suggestions.length > 0) { chatHistory.push({ role: 'ai', content: responseHtml, isSuggestion: false }); chatHistory.push({ role: 'ai', content: parsedData.suggestions, isSuggestion: true }); } else { chatHistory.push({ role: 'ai', content: "Sorry, I couldn't get a response. Please try again.", isSuggestion: false }); } } catch (error) { console.error('Error fetching from Gemini API:', error); chatHistory.push({ role: 'ai', content: "An error occurred. Please check your network connection.", isSuggestion: false }); } finally { hideTypingIndicator(); displayChatMessages(chatHistory); } }; // --- Event Listeners --- window.addEventListener('load', initFirebase); studentListEl.addEventListener('click', (e) => { const btn = e.target.closest('.daily-attendance-btn'); if (btn) { const studentId = btn.dataset.id; const status = btn.dataset.status; const date = btn.dataset.date; logAttendance(studentId, date, status); } const chatBtn = e.target.closest('.chat-btn'); if (chatBtn) { const studentId = chatBtn.dataset.id; const studentData = students.find(s => s.id === studentId); if (studentData) { openChat(studentData); } } }); prevWeekBtn.addEventListener('click', goToPreviousWeek); nextWeekBtn.addEventListener('click', goToNextWeek); closeChatBtnEl.addEventListener('click', closeChat); chatFormEl.addEventListener('submit', handleChatSubmit);