<
>
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 `
`;
}).join('')}
${isAbsent && !isCurrentWeekHoliday ? `
Chat
` : ''}
`;
});
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' })}
${absentStudents.map(student => {
const status = isCurrentWeekHoliday ? 'SH' : student.history.find(h => h.date === dateStr)?.status;
const statusLabel = status === 'SH' ? ' (School Holiday)' : '';
return `${student.name} (${student.level})${statusLabel} `;
}).join('')}
`;
}
});
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:
${msg.content.map(item => `${item} `).join('')}
`;
}
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);