Panlaitan QR Attendance

Cross-Platform QR Attendance System · SY 2026–2027

Use the official Supabase Admin account for this QR Attendance System.
`); w.document.close(); } `); w.document.close(); } function renderLearners(){ populateSectionFilter(window.__PES_ACTIVE_SECTIONS); const q=(document.getElementById("learnerSearch").value||"").toLowerCase().trim(); const selected=document.getElementById("sectionFilter").value; let list=state.learners.slice(); if(selected) list=list.filter(l=>l.section===selected); if(state.user==="adviser") list=list.filter(l=>l.grade==="Grade 6"&&l.section==="Amethyst"); list=list.filter(l=>(l.name+" "+l.id+" "+(l.lrn||"")+" "+l.section).toLowerCase().includes(q)); document.getElementById("learnerTable").innerHTML=list.map(l=>` ${l.id}${l.name}${l.gender||""}${l.grade}${l.section}${l.parent}${l.contact}
`).join("")||'No learners enrolled in the selected section.'; } function getAttendanceSectionGrade(section){ const rows=window.__PES_ACTIVE_SECTION_ROWS||[]; const match=rows.find(x=>x.name===section); if(match?.grade) return match.grade; // Fallback for older/local records if a section mapping is not available. const learner=state.learners.find(l=>l.section===section && l.grade); return learner?.grade || ""; } function renderAttendance(){ const dateEl=document.getElementById("attDate"); const sectionEl=document.getElementById("attSection"); const date=(dateEl&&dateEl.value)||today; let sec=(sectionEl&§ionEl.value)||""; if(state.user==="adviser"){ sec=(state.currentTeacherAssignment?.section||"").trim(); if(sectionEl){sectionEl.value=sec;sectionEl.disabled=true;} }else if(sectionEl){ sectionEl.disabled=false; } const grade=getAttendanceSectionGrade(sec); const ls=state.learners.filter(l=> l.section===sec && (!grade || l.grade===grade) ); const titleDate=date; const displayDate=date ? new Date(date+"T00:00:00").toLocaleDateString("en-US",{year:"numeric",month:"long",day:"numeric"}) : ""; const title=document.getElementById("attTitle"); const table=document.getElementById("attendanceTable"); if(title) title.textContent=grade ? `${grade} – ${sec} · ${titleDate}` : `${sec} · ${titleDate}`; if(!ls.length){ table.innerHTML='No learners are enrolled in this section.'; }else{ table.innerHTML=ls.map((l,i)=>{ const scans=state.scans.filter(s=>s.id===l.id&&s.date===date); const get=m=>scans.find(s=>s.mode===m)?.time||"—"; const morningIn=scans.find(s=>s.mode==="MORNING IN"); const status=morningIn ? (morningIn.time>"07:30 AM" ? "Late" : "Present") : (scans.length ? "Present" : "Absent"); return ` ${i+1}${l.name} ${get("MORNING IN")} ${get("MORNING OUT")} ${get("AFTERNOON IN")} ${get("AFTERNOON OUT")} ${status} `; }).join(""); } const note=document.getElementById("attendanceDateNote"); if(note) note.textContent=`Attendance for ${displayDate}`; return {date,grade,sec,count:ls.length}; } function getAttendanceRows(){ const date=document.getElementById("attDate").value||today; const sec=document.getElementById("attSection").value; const grade=getAttendanceSectionGrade(sec); const ls=state.learners.filter(l=>l.section===sec && (!grade || l.grade===grade)); const rows=[["No.","Learner","Morning In","Morning Out","Afternoon In","Afternoon Out","Status"]]; ls.forEach((l,i)=>{ const a=state.scans.filter(s=>s.id===l.id&&s.date===date); const get=m=>a.find(s=>s.mode===m)?.time||""; const morningIn=a.find(s=>s.mode==="MORNING IN"); const st=morningIn?(morningIn.time>"07:30 AM"?"Late":"Present"):(a.length?"Present":"Absent"); rows.push([i+1,l.name,get("MORNING IN"),get("MORNING OUT"),get("AFTERNOON IN"),get("AFTERNOON OUT"),st]); }); return {rows,grade,sec,date,count:ls.length}; } function downloadAttendanceSheet(){ const result=getAttendanceRows(); if(!result.count){ alert("There are no learners in the selected grade and section."); return; } const safeSec=result.sec.replace(/[^\w\-]+/g,"_"); const selectedDate=new Date(result.date+"T00:00:00"); const monthName=selectedDate.toLocaleDateString("en-US",{month:"long",year:"numeric"}); // Find the adviser/teacher assigned to the selected grade and section. const teacherRecord=(state.teachers||[]).find(t=> t.grade===result.grade && t.section.toLowerCase()===result.sec.toLowerCase() ); const teacherName=teacherRecord ? teacherRecord.name : "____________________________"; if(typeof XLSX!=="undefined"){ const rows=result.rows; const wb=XLSX.utils.book_new(); // Formal DepEd-style attendance header. const formalRows=[ ["PANLAITAN ELEMENTARY SCHOOL"], ["DAILY ATTENDANCE SHEET"], ["School Year: 2026–2027"], [""], ["Name of School","Panlaitan Elementary School","Name of Teacher",teacherName], ["Month",monthName,"Grade Level",result.grade], ["Section",result.sec,"Date",result.date], [""], ...rows ]; const ws=XLSX.utils.aoa_to_sheet(formalRows); // Merge title rows and make the information area formal. ws["!merges"]=[ {s:{r:0,c:0},e:{r:0,c:6}}, {s:{r:1,c:0},e:{r:1,c:6}}, {s:{r:2,c:0},e:{r:2,c:6}}, {s:{r:3,c:0},e:{r:3,c:6}} ]; ws["!cols"]=[ {wch:8},{wch:32},{wch:17},{wch:17},{wch:18},{wch:18},{wch:13} ]; // Print setup for a formal paper copy. ws["!pageSetup"]={ orientation:"landscape", paperSize:9, fitToWidth:1, fitToHeight:0 }; ws["!sheetPr"]={pageSetUpPr:{fitToPage:true}}; ws["!printOptions"]={horizontalCentered:true}; ws["!margins"]={left:0.25,right:0.25,top:0.5,bottom:0.5,header:0.2,footer:0.2}; ws["!autofilter"]={ref:`A9:G${8+rows.length}`}; ws["!freeze"]={xSplit:0,ySplit:9}; // Formatting. const titleStyle={ font:{bold:true,sz:16}, alignment:{horizontal:"center",vertical:"center"} }; const subtitleStyle={ font:{bold:true,sz:13}, alignment:{horizontal:"center",vertical:"center"} }; const syStyle={ font:{italic:true,sz:11}, alignment:{horizontal:"center",vertical:"center"} }; [0].forEach(r=>ws[XLSX.utils.encode_cell({r,c:0})].s=titleStyle); ws[XLSX.utils.encode_cell({r:1,c:0})].s=subtitleStyle; ws[XLSX.utils.encode_cell({r:2,c:0})].s=syStyle; // Information labels. for(let r=4;r<=6;r++){ [0,2].forEach(c=>{ const cell=ws[XLSX.utils.encode_cell({r,c})]; cell.s={ font:{bold:true,sz:11}, alignment:{vertical:"center"}, fill:{fgColor:{rgb:"E9EEF3"}}, border:{ top:{style:"thin"},bottom:{style:"thin"}, left:{style:"thin"},right:{style:"thin"} } }; }); [1,3].forEach(c=>{ const cell=ws[XLSX.utils.encode_cell({r,c})]; cell.s={ font:{sz:11}, alignment:{vertical:"center"}, border:{ top:{style:"thin"},bottom:{style:"thin"}, left:{style:"thin"},right:{style:"thin"} } }; }); } // Attendance table header is row 9 (zero-based index 8). const headerRow=8; for(let c=0;c<7;c++){ const cell=ws[XLSX.utils.encode_cell({r:headerRow,c})]; cell.s={ font:{bold:true,sz:11}, alignment:{horizontal:"center",vertical:"center",wrapText:true}, fill:{fgColor:{rgb:"D9E2F3"}}, border:{ top:{style:"thin"},bottom:{style:"thin"}, left:{style:"thin"},right:{style:"thin"} } }; } // Attendance body borders/alignment. for(let r=9;r<8+rows.length;r++){ for(let c=0;c<7;c++){ const cell=ws[XLSX.utils.encode_cell({r,c})]; cell.s={ font:{sz:10}, alignment:{horizontal:c===1?"left":"center",vertical:"center"}, border:{ top:{style:"thin"},bottom:{style:"thin"}, left:{style:"thin"},right:{style:"thin"} } }; } } ws["!rows"]=[ {hpt:24},{hpt:21},{hpt:18},{hpt:8}, {hpt:21},{hpt:21},{hpt:21},{hpt:8},{hpt:30} ]; XLSX.utils.book_append_sheet(wb,ws,"Attendance Sheet"); XLSX.writeFile(wb,`Attendance_Sheet_${safeSec}_${monthName.replace(/\s+/g,"_")}.xlsx`); }else{ // Fallback CSV with the same formal header information. const formalCsv=[ ["PANLAITAN ELEMENTARY SCHOOL"], ["DAILY ATTENDANCE SHEET"], ["School Year: 2026–2027"], [""], ["Name of School","Panlaitan Elementary School","Name of Teacher",teacherName], ["Month",monthName,"Grade Level",result.grade], ["Section",result.sec,"Date",result.date], [""], ...result.rows ]; csv(`Attendance_Sheet_${safeSec}_${monthName.replace(/\s+/g,"_")}.csv`,formalCsv); } } async function generateAttendanceSheet(){ const msg=document.getElementById("attendanceGenerationMsg"); const btn=document.getElementById("generateAttendance"); const oldText=btn?btn.textContent:"Generate Attendance Sheet"; try{ if(btn){btn.disabled=true;btn.textContent="Generating…";} // Ensure the latest learners, active School Year sections, and adviser assignment are loaded. if(typeof window.loadLearnersFinal==="function") await window.loadLearnersFinal(); if(typeof window.refreshSectionsFromActiveSchoolYear==="function") await window.refreshSectionsFromActiveSchoolYear(); const result=renderAttendance(); if(!result.count){ msg.style.display="block"; msg.innerHTML='
Attendance sheet generated, but no learners were found.
Check the selected Grade and Section.
'; return; } msg.style.display="block"; msg.innerHTML=`
Attendance Sheet Generated Successfully.
${result.grade} – ${result.sec} · ${result.date}
${result.count} learner(s) loaded. QR scan times and attendance status are now displayed below. You can download this generated sheet using Download Attendance Sheet.
`; document.getElementById("attendanceTable").scrollIntoView({behavior:"smooth",block:"nearest"}); }catch(err){ console.error("Attendance generation error:",err); msg.style.display="block"; msg.innerHTML='
Unable to generate the attendance sheet.
'+escapeHtml(err?.message||"Please check the selected date and section.")+'
'; }finally{ if(btn){btn.disabled=false;btn.textContent=oldText;} } } function renderLogs(){ document.getElementById("logTable").innerHTML=state.scans.slice().reverse().map(s=>`${s.date}${s.time}${s.id}${s.name}${s.grade}${s.section}${s.mode||s.type}`).join("")||'No scan records.'; } function renderSms(){ const tbody=document.getElementById("smsTable"); if(!tbody)return; tbody.innerHTML=state.sms.slice().reverse().map((s,idx)=>{ const realIndex=state.sms.length-1-idx; const cls=s.status==="SENT"?"present":(s.status==="FAILED"?"absent":"late"); const action=(s.status==="FAILED"||s.status==="QUEUED"||s.status==="NOT CONFIGURED") ? `` : "—"; return `${s.date} ${s.time}${escapeHtml(s.learner)}${escapeHtml(s.parent)}${escapeHtml(s.contact)}${escapeHtml(s.message)}${escapeHtml(s.status||"QUEUED")}${action}`; }).join("")||'No SMS notifications yet.'; tbody.querySelectorAll('.smsRetry').forEach(btn=>btn.addEventListener('click',async()=>{ const item=state.sms[Number(btn.dataset.index)]; if(item) await sendSmsQueueItem(item); })); } function csv(filename,rows){ const csvEscape = value => { const s = String(value ?? ""); return '"' + s.replace(/"/g,'""').replace(/\\r?\\n/g," ") + '"'; }; const text = rows.map(row => row.map(csvEscape).join(",")).join("\\r\\n"); const blob = new Blob(["\\uFEFF", text], {type:"text/csv;charset=utf-8;"}); const a = document.createElement("a"); const url = URL.createObjectURL(blob); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(url),1000); } document.getElementById("logout").onclick=()=>location.reload(); document.getElementById("scanBtn").onclick=scan; document.getElementById("closeQR").onclick=closeQRModal; document.getElementById("printQR").onclick=()=>printLearnerQR(activeQRLearner); document.getElementById("printAllQR").onclick=printAllLearnerQRs; document.getElementById("downloadAllQR").onclick=downloadAllLearnerQRs; document.getElementById("qrModal").addEventListener("click",e=>{if(e.target.id==="qrModal")closeQRModal();}); document.querySelectorAll(".scan-mode").forEach(b=>b.addEventListener("click",()=>setScanMode(b.dataset.mode))); function escapeHtml(v){ return String(v??"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); } function renderTeachers(){ const tbody=document.getElementById("teacherTable"); if(!tbody)return; const q=(document.getElementById("teacherSearch")?.value||"").trim().toLowerCase(); const list=(state.teachers||[]).map((t,i)=>({...t,_index:i})).filter(t=> !q || String(t.name||"").toLowerCase().includes(q) || String(t.section||"").toLowerCase().includes(q) || String(t.grade||"").toLowerCase().includes(q) || String(t.role||"").toLowerCase().includes(q) ); tbody.innerHTML=list.length ? list.map((t,n)=>` ${n+1} ${escapeHtml(t.name)} ${escapeHtml(t.role)} ${escapeHtml(t.grade)} ${escapeHtml(t.section)} `).join("") : 'No teacher assignments found.'; tbody.querySelectorAll(".teacher-edit").forEach(btn=>{ btn.addEventListener("click",()=>{ const i=Number(btn.dataset.index), t=state.teachers[i]; if(!t)return; document.getElementById("teacherName").value=t.name||""; document.getElementById("teacherRole").value=t.role||"Adviser"; document.getElementById("teacherGrade").value=t.grade||"Grade 6"; document.getElementById("teacherSection").value=t.section||""; const save=document.getElementById("saveTeacher"); save.dataset.editIndex=String(i); save.textContent="Update Teacher Assignment"; document.getElementById("teacherMsg").innerHTML='
Editing the selected teacher assignment.
'; document.getElementById("teacherName").focus(); }); }); tbody.querySelectorAll(".teacher-delete").forEach(btn=>{ btn.addEventListener("click",()=>{ const i=Number(btn.dataset.index), t=state.teachers[i]; if(!t)return; if(!confirm(`Delete the teacher assignment for ${t.name} – ${t.grade} ${t.section}?`))return; state.teachers.splice(i,1); const save=document.getElementById("saveTeacher"); if(save.dataset.editIndex!==undefined)delete save.dataset.editIndex; save.textContent="Save Teacher Assignment"; document.getElementById("teacherMsg").innerHTML='
Teacher assignment deleted successfully.
'; renderTeachers(); }); }); } document.getElementById("teacherSearch").addEventListener("input",renderTeachers); setTimeout(()=>{if(typeof refreshSectionsFromActiveSchoolYear==="function")refreshSectionsFromActiveSchoolYear();},700); setTimeout(()=>{if(typeof refreshSectionsFromActiveSchoolYear==="function")refreshSectionsFromActiveSchoolYear();},1800); document.getElementById("saveTeacher").onclick=()=>{ const name=document.getElementById("teacherName").value.trim(); const role=document.getElementById("teacherRole").value; const grade=document.getElementById("teacherGrade").value; const section=document.getElementById("teacherSection").value.trim(); const msg=document.getElementById("teacherMsg"); const btn=document.getElementById("saveTeacher"); if(!name||!section){ msg.innerHTML='
Please enter the teacher name and section.
'; return; } const editIndex=btn.dataset.editIndex!==undefined?parseInt(btn.dataset.editIndex,10):-1; const duplicate=state.teachers.some((t,i)=>i!==editIndex&&t.name.toLowerCase()===name.toLowerCase()&&t.grade===grade&&t.section.toLowerCase()===section.toLowerCase()); if(duplicate){ msg.innerHTML='
This teacher assignment already exists.
'; return; } if(editIndex>=0 && state.teachers[editIndex]){ state.teachers[editIndex]={name,role,grade,section}; delete btn.dataset.editIndex; btn.textContent="Save Teacher Assignment"; msg.innerHTML='
Teacher assignment updated successfully.
'; }else{ state.teachers.push({name,role,grade,section}); msg.innerHTML='
Teacher assignment saved successfully.
'; } document.getElementById("teacherName").value=""; document.getElementById("teacherSection").value=""; renderTeachers(); try{ loadTeacherAccounts(); }catch(e){ console.warn(e); } }; document.getElementById("scanInput").addEventListener("keydown",e=>{if(e.key==="Enter")scan()}); document.getElementById("learnerSearch").addEventListener("input",renderLearners); document.getElementById("newGrade")?.addEventListener("change",()=>refreshNewLearnerGradeSections(document.getElementById("newGrade")?.value||"")); document.getElementById("attDate").addEventListener("change",renderAttendance); document.getElementById("attSection").addEventListener("change",()=>{ renderAttendance(); }); document.getElementById("sectionFilter").addEventListener("change",renderLearners); setTimeout(()=>{if(typeof refreshSectionsFromActiveSchoolYear==="function")refreshSectionsFromActiveSchoolYear();},500); // Keep the section selector backed by the active School Year. setTimeout(()=>{ try{ if(document.getElementById("attendance")?.classList.contains("active")) refreshAttendanceData().catch(()=>{}); }catch(e){} },1200); document.getElementById("addLearner").onclick=()=>openLearnerForAdd(); document.getElementById("closeModal").onclick=()=>closeLearnerModal(); function resetLearnerForm(){ const ids=["newLRN","newName","newParent","newContact"]; ids.forEach(id=>{const el=document.getElementById(id);if(el)el.value="";}); const g=document.getElementById("newGender"); if(g)g.value="M"; const title=document.getElementById("learnerModalTitle"); if(title)title.textContent="Add Learner"; const save=document.getElementById("saveLearner"); if(save){save.textContent="Save";delete save.dataset.editId;} } function openLearnerForAdd(){ resetLearnerForm(); refreshNewLearnerGradeSections(); // Advisers are locked to their own registered Grade + Section. if(state.user==="adviser" && state.currentTeacherAssignment){ refreshNewLearnerGradeSections(state.currentTeacherAssignment.grade,state.currentTeacherAssignment.section); } const modal=document.getElementById("modal"); if(modal)modal.classList.add("show"); } function closeLearnerModal(){ const modal=document.getElementById("modal"); if(modal)modal.classList.remove("show"); } function setLearnerField(id,value){const el=document.getElementById(id);if(el)el.value=value??"";} function editLearner(id){ const l=state.learners.find(x=>String(x.id||x.lrn)===String(id)); if(!l){alert("Learner record not found.");return;} // Advisers may edit only their own assigned section. Admin can edit any learner. if(state.user==="adviser" && (String(l.grade||"").toLowerCase()!==String(state.currentTeacherAssignment?.grade||"Grade 6").toLowerCase() || String(l.section||"").toLowerCase()!==String(state.currentTeacherAssignment?.section||"Amethyst").toLowerCase())){ alert("You can only edit learners in your assigned advisory class."); return; } const title=document.getElementById("learnerModalTitle"); if(title)title.textContent="Edit Learner"; setLearnerField("newLRN",l.lrn||l.id); setLearnerField("newName",l.name); setLearnerField("newParent",l.parent); setLearnerField("newContact",l.contact); const gender=document.getElementById("newGender"); if(gender)gender.value=l.gender||"M"; refreshNewLearnerGradeSections(l.grade||"",l.section||""); const grade=document.getElementById("newGrade"); if(grade)grade.value=l.grade||""; const section=document.getElementById("newSection"); if(section)section.value=l.section||""; const save=document.getElementById("saveLearner"); if(save){save.textContent="Update Learner";save.dataset.editId=String(l.id||l.lrn);} const modal=document.getElementById("modal"); if(modal)modal.classList.add("show"); } async function deleteLearner(id){ const l=state.learners.find(x=>String(x.id||x.lrn)===String(id)); if(!l){alert("Learner record not found.");return;} if(state.user==="adviser" && (String(l.grade||"").toLowerCase()!==String(state.currentTeacherAssignment?.grade||"Grade 6").toLowerCase() || String(l.section||"").toLowerCase()!==String(state.currentTeacherAssignment?.section||"Amethyst").toLowerCase())){ alert("You can only delete learners in your assigned advisory class."); return; } const ok=confirm(`Delete ${l.name || "this learner"}\n\nThis will remove the learner from the current browser data. Continue?`); if(!ok)return; state.learners=state.learners.filter(x=>String(x.id||x.lrn)!==String(id)); state.scans=state.scans.filter(x=>String(x.id)!==String(id)); state.sms=state.sms.filter(x=>String(x.learnerId)!==String(id)); window.state=state; renderLearners(); renderDashboard(); renderAttendance(); renderLogs(); renderSms(); } document.getElementById("saveLearner").onclick=()=>{ const lrn=(document.getElementById("newLRN").value||"").trim(); const l={id:lrn,lrn,name:document.getElementById("newName").value.trim(),gender:document.getElementById("newGender").value,grade:document.getElementById("newGrade").value,section:document.getElementById("newSection").value,parent:document.getElementById("newParent").value.trim(),contact:document.getElementById("newContact").value.trim()}; if(!/^\d{12}$/.test(lrn)){alert("Please enter a valid 12-digit LRN.");return} if(!l.name||!l.parent||!l.contact){alert("Please complete the learner and parent information.");return} const btn=document.getElementById("saveLearner"); const editId=btn.dataset.editId; if(editId){ const index=state.learners.findIndex(x=>String(x.id||x.lrn)===String(editId)); if(index<0){alert("Learner record not found.");return;} const duplicate=state.learners.some((x,i)=>i!==index && String(x.lrn||x.id||"").trim()===lrn); if(duplicate){alert("This LRN already exists.");return;} // Advisers cannot move a learner outside their assigned class. if(state.user==="adviser" && (l.grade!==state.currentTeacherAssignment?.grade || l.section!==state.currentTeacherAssignment?.section)){ alert("Advisers cannot change a learner to another Grade/Section. The learner must remain in your assigned advisory class.");return; } state.learners[index]=l; }else{ if(state.learners.some(x=>String(x.lrn||x.id||"").trim()===lrn)){alert("This LRN already exists.");return} if(state.user==="adviser" && (l.grade!==state.currentTeacherAssignment?.grade || l.section!==state.currentTeacherAssignment?.section)){ alert("Learners added by an adviser must use the adviser's assigned Grade and Section.");return; } state.learners.push(l); } closeLearnerModal(); resetLearnerForm(); renderLearners(); renderDashboard(); renderAttendance(); }; function downloadAdviserTemplate(){ const isAdv=state.user==="adviser"; const selected=document.getElementById("sectionFilter")?.value||""; const section=isAdv?"Amethyst":(selected||"Amethyst"); const grade=isAdv?"Grade 6":(state.learners.find(l=>l.section===section)?.grade||""); // Create an Excel-compatible .xls file directly in the browser. // This works from a local HTML file and does not require any CDN/library. const esc=v=>String(v??"") .replace(/&/g,"&") .replace(//g,">") .replace(/"/g,"""); const table=`
LRN Learner Name Gender Grade Section Parent/Guardian Contact Number
${esc(grade)} ${esc(section)}
`; const excelHtml=` ${table}<\/body>`; try{ const blob=new Blob(["\ufeff",excelHtml],{type:"application/vnd.ms-excel"}); const filename="PES_Adviser_Learner_Template_"+section.replace(/[^\w-]+/g,"_")+".xls"; // IE/old Edge compatibility. if(window.navigator && typeof window.navigator.msSaveOrOpenBlob==="function"){ window.navigator.msSaveOrOpenBlob(blob,filename); return; } const url=URL.createObjectURL(blob); const a=document.createElement("a"); a.href=url; a.download=filename; a.rel="noopener"; a.style.display="none"; document.body.appendChild(a); a.click(); setTimeout(function(){ a.remove(); URL.revokeObjectURL(url); },1000); }catch(err){ console.error(err); // Final fallback: open the template in a new tab so it can be saved in Excel. const w=window.open("","_blank"); if(!w){ alert("The browser blocked the template download. Please allow downloads/pop-ups for this file."); return; } w.document.open(); w.document.write(excelHtml); w.document.close(); alert("The Excel template was opened in a new tab. In Excel, use Save As to save it as an Excel file."); } } function parseLearnerRows(rows){ if(!rows||!rows.length) return; const headers=rows[0].map(x=>String(x??"").trim().toLowerCase()); const idx=(...names)=>{for(const n of names){const i=headers.indexOf(n.toLowerCase());if(i>=0)return i}return -1}; const lrnI=idx("lrn"), nameI=idx("learner name","name"), genderI=idx("gender","sex"), gradeI=idx("grade","grade level"), secI=idx("section"), parentI=idx("parent/guardian","parent","guardian"), contactI=idx("contact number","contact","parent contact"); if(lrnI<0||nameI<0||gradeI<0||secI<0){ alert("Invalid template. Required columns: LRN, Learner Name, Gender, Grade, Section, Parent/Guardian, Contact Number."); return; } let added=0, skipped=0, restricted=0; for(let r=1;rString(x.lrn||"").trim()===lrn); const duplicateName=state.learners.some(x=>String(x.name||"").trim().toLowerCase()===name.toLowerCase() && String(x.grade||"")===grade && String(x.section||"")===section); if(duplicateLrn||duplicateName){skipped++;continue;} const id=lrn; state.learners.push({ id, lrn, name, lrn, gender:genderI>=0?String(row[genderI]??"").trim():"", grade, section, parent:parentI>=0?String(row[parentI]??"").trim():"", contact:contactI>=0?String(row[contactI]??"").trim():"" }); added++; } renderLearners(); renderDashboard(); alert(`Upload complete.\nAdded: ${added}\nDuplicates skipped: ${skipped}\nRestricted rows skipped: ${restricted}\n\nThe supplied LRN is now used as the learner ID and QR code value.`); } function handleLearnerFile(file){ if(!file)return; const name=(file.name||"").toLowerCase(); const isCsv=name.endsWith(".csv"); const isExcel=name.endsWith(".xlsx")||name.endsWith(".xls"); if(!isCsv && !isExcel){ alert("Please select an Excel (.xlsx/.xls) or CSV (.csv) learner list."); return; } const reader=new FileReader(); reader.onerror=()=>alert("The learner file could not be opened. Please try the downloaded Adviser Excel Template."); reader.onload=e=>{ try{ if(isCsv){ const text=String(e.target.result||""); const rows=[]; let row=[], cell="", quoted=false; for(let i=0;iString(v).trim()!==""))rows.push(row); row=[]; continue; } cell+=ch; } if(cell!==""||row.length){ row.push(cell); if(row.some(v=>String(v).trim()!==""))rows.push(row); } parseLearnerRows(rows); return; } if(typeof XLSX==="undefined"){ alert("Excel support is unavailable. Please connect to the internet and reload the system, or save the Excel file as CSV and upload the CSV."); return; } const data=new Uint8Array(e.target.result); const wb=XLSX.read(data,{type:"array",cellDates:false}); if(!wb.SheetNames.length){ alert("The Excel file contains no worksheet."); return; } const ws=wb.Sheets[wb.SheetNames[0]]; const rows=XLSX.utils.sheet_to_json(ws,{header:1,defval:"",raw:false}); parseLearnerRows(rows); }catch(err){ console.error(err); alert("Could not read the learner file. Please use the downloaded Adviser Excel Template and do not change the column headings."); } }; if(isCsv) reader.readAsText(file); else reader.readAsArrayBuffer(file); } document.getElementById("exportSection").onclick=()=>{ const sec=document.getElementById("sectionFilter").value; if(!sec){alert("Please select a section first.");return;} let list=state.learners.filter(l=>l.section===sec); if(state.user==="adviser") list=list.filter(l=>l.grade==="Grade 6"&&l.section==="Amethyst"); if(!list.length){alert(`No learners are enrolled in ${sec}.`);return;} csv(`learners_${sec.replace(/\\s+/g,"_")}.csv`,[ ["Learner ID","LRN","Learner Name","Gender","Grade","Section","Parent/Guardian","Contact Number"], ...list.map(l=>[l.id,l.lrn||"",l.name,l.gender||"",l.grade,l.section,l.parent,l.contact]) ]); }; document.getElementById("exportLogs").onclick=()=>csv("qr_scan_log.csv",[["Date","Time","Learner ID","Learner","Grade","Section","Scan Type"],...state.scans.map(s=>[s.date,s.time,s.id,s.name,s.grade,s.section,s.type])]); document.getElementById("downloadAttendanceSheet").addEventListener("click",downloadAttendanceSheet); document.getElementById("generateAttendance").addEventListener("click",function(e){ e.preventDefault(); generateAttendanceSheet().catch(err=>console.error(err)); }); async function refreshAttendanceData(){ const btn=document.getElementById("refreshAttendance"); const oldText=btn?btn.textContent:"↻ Refresh Attendance"; const msg=document.getElementById("attendanceGenerationMsg"); if(btn){btn.disabled=true;btn.textContent="Refreshing…";} if(msg){msg.style.display="block";msg.innerHTML='
Refreshing attendance data from the active School Year…
';} try{ if(!window.state && typeof state!=="undefined") window.state=state; if(!window.state) throw new Error("Application state is not initialized yet. Please wait a moment and try again."); if(typeof window.loadLearnersFinal==="function") await window.loadLearnersFinal(); else if(typeof loadLearnersFinal==="function") await loadLearnersFinal(); if(typeof window.refreshSectionsFromActiveSchoolYear==="function") await window.refreshSectionsFromActiveSchoolYear(); const assignment=window.state.currentTeacherAssignment; if(window.state.user==="adviser" && assignment){ const sel=document.getElementById("attSection"); if(sel){sel.value=assignment.section||"";sel.disabled=true;} } const result=renderAttendance(); if(msg){ msg.innerHTML='
Attendance refreshed successfully.
'+escapeHtml((result.grade?result.grade+' – ':'')+result.sec)+' · '+escapeHtml(result.date)+'
'+result.count+' learner(s) loaded.
'; } return result; }catch(err){ console.error("Attendance refresh error:",err); if(msg) msg.innerHTML='
Could not refresh attendance.
'+escapeHtml(err?.message||String(err))+'
'; throw err; }finally{ if(btn){btn.disabled=false;btn.textContent=oldText;} } } document.getElementById("refreshAttendance").addEventListener("click",async function(e){ e.preventDefault(); try{await refreshAttendanceData();}catch(e){} }); document.getElementById("exportAttendance").onclick=()=>{ const result=getAttendanceRows(); const date=result.date; const grade=result.grade; const sec=result.sec; const ls=state.learners.filter(l=>l.section===sec && (!grade || l.grade===grade)); csv(`attendance_${sec}_${date}.csv`,[ ["No.","Learner","Morning In","Morning Out","Afternoon In","Afternoon Out","Status"], ...ls.map((l,i)=>{ const a=state.scans.filter(s=>s.id===l.id&&s.date===date); const get=m=>a.find(s=>s.mode===m)?.time||""; const st=a.find(s=>s.mode==="MORNING IN")?(a.find(s=>s.mode==="MORNING IN").time>"07:30 AM"?"Late":"Present"):(a.length?"Present":"Absent"); return[i+1,l.name,get("MORNING IN"),get("MORNING OUT"),get("AFTERNOON IN"),get("AFTERNOON OUT"),st]; }) ]); }; document.getElementById("sf2Btn").onclick=async function(e){ e.preventDefault(); const btn=this; const oldText=btn.textContent; btn.disabled=true; btn.textContent="Generating SF2…"; try{ if(typeof refreshSF2Access==="function") await refreshSF2Access(); generateSF2(); }catch(err){ console.error("SF2 preview error:",err); const out=document.getElementById("sf2Preview"); if(out) out.innerHTML='
Unable to generate the SF2 preview.
'+escapeHtml(err?.message||String(err))+'
'; }finally{ btn.disabled=false; btn.textContent=oldText; } }; function generateSF2(){ const monthName=document.getElementById("sf2Month")?.value||"August"; const selected=window.__PES_SF2_ASSIGNMENT||{}; let section=(document.getElementById("sf2Section")?.value||selected.section||"").trim(); let grade=(document.getElementById("sf2Grade")?.value||selected.grade||"").trim(); // Advisers are always locked to their own active assignment. if(state.user==="adviser"){ const adviserAssignment=state.currentTeacherAssignment||selected||{}; section=String(adviserAssignment.section||"").trim(); grade=String(adviserAssignment.grade||"").trim(); } if(!section){ document.getElementById("sf2Preview").innerHTML='
No adviser assignment was found for the active School Year.
Please ask the Admin to assign your teacher account to a Grade and Section.
'; return; } const year=Number((document.getElementById("sf2SchoolYear")?.value||"2026–2027").replace(/[^0-9].*$/,'').trim())||2026; const monthIndex=new Date(`${monthName} 1, ${year}`).getMonth(); const days=new Date(year,monthIndex+1,0).getDate(); const working=[...Array(days)].map((_,i)=>new Date(year,monthIndex,i+1)).filter(d=>![0,6].includes(d.getDay())); const ls=(state.learners||[]).filter(l=> String(l.section||"").trim().toLowerCase()===section.toLowerCase() && (!grade||String(l.grade||"").trim().toLowerCase()===grade.toLowerCase()) ); // Normalize gender so both M/Male and F/Female are handled correctly. const isFemale=l=>/^(f|female)$/i.test(String(l.gender||"").trim()); const isMale=l=>/^(m|male)$/i.test(String(l.gender||"").trim()); const males=ls.filter(isMale); const females=ls.filter(isFemale); const unknownGender=ls.filter(l=>!isMale(l)&&!isFemale(l)); // If a legacy record has no recognized gender, keep it visible with the male group // rather than silently dropping the learner from the official report. males.push(...unknownGender); const cell=(l,d)=>{ const iso=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; const has=(state.scans||[]).some(sc=>String(sc.id)===String(l.id)&&sc.date===iso&&String(sc.section||"").trim().toLowerCase()===section.toLowerCase()); return has?"":"x"; }; const dailyTotals=learners=>working.map(d=>learners.reduce((n,l)=>n+(cell(l,d)===""?1:0),0)); const totalPresent=learners=>dailyTotals(learners).reduce((a,b)=>a+b,0); const totalPossible=learners.length*working.length; const totalAbsent=totalPossible-totalPresent(learners); let h=`
School Form 2 (SF2) Daily Attendance Report of Learners
(This replaces Form 1, Form 2 & STS Form 4 - Absenteeism and Dropout Profile)
School ID: 110955School Year: ${escapeHtml(document.getElementById("sf2SchoolYear")?.value||"2026-2027")}Report for the Month: ${escapeHtml(monthName)}Name of School: PANLAITAN ELEMENTARY SCHOOLGrade Level: ${escapeHtml(grade)}Section: ${escapeHtml(section)}
`; h+=`
${working.map(()=>``).join("")}${working.map(d=>``).join("")}${working.map(d=>``).join("")}`; const groupRow=(label,cls="")=>{ h+=``; }; const subtotalRow=(label,learners,cls="")=>{ const daily=dailyTotals(learners); const present=daily.reduce((a,b)=>a+b,0); const absent=(learners.length*working.length)-present; h+=`${daily.map(v=>``).join("")}`; }; const learnerRows=(learners)=>{ learners.forEach((l,i)=>{ const daily=dailyTotals([l]); const pres=daily.reduce((a,b)=>a+b,0); const abs=working.length-pres; h+=`${working.map(d=>``).join("")}`; }); }; if(males.length){ groupRow("MALE LEARNERS","malegroup"); learnerRows(males); subtotalRow("<=== MALE | TOTAL Per Day ===>",males,"malesubtotal"); } if(females.length){ groupRow("FEMALE LEARNERS","femalegroup"); learnerRows(females); subtotalRow("<=== FEMALE | TOTAL Per Day ===>",females,"femalesubtotal"); } if(males.length||females.length){ const combined=[...males,...females]; subtotalRow("COMBINED TOTAL Per Day",combined,"combinedtotal"); }else{ h+=``; } h+=`
No.NAME
(Last Name, First Name, Middle Name)
${d.getDate()}Total
Present
Total
Absent
${["S","M","T","W","TH","F"][d.getDay()]||""}
${label}
${label}${v}${present}${absent}
${i+1}${escapeHtml(l.name||"")}${cell(l,d)}${pres}${abs}
No learners are enrolled in this Grade and Section.
CODES FOR CHECKING ATTENDANCE: (blank) - Present; (x) - Absent; Tardy - Late Comer.

Gender separation: Male learners are listed first, followed by Female learners. Each gender has a daily present subtotal, followed by the combined daily total.

Daily totals: The subtotal under each date is the number of learners marked Present (blank) for that day. Total Present/Absent are the sums across all working days.

Access: Advisers can generate only the SF2 for their own active School Year assignment. Admin can generate reports for any active School Year section.
`; document.getElementById("sf2Preview").innerHTML=h; } async function loadCurrentAdviserAssignment(){ if(state.user!=="adviser") return null; const db=window.__PES_LOGIN_DB; const auth=window.__PES_AUTH_USER; if(!db||!auth?.id) return null; try{ // The logged-in Supabase Auth account is the authoritative identity. // Match the teacher profile by auth_user_id first, then by the Auth email. const sy=await db.from("school_years") .select("id,school_year,is_active") .eq("is_active",true) .limit(1) .maybeSingle(); if(sy.error||!sy.data) return null; const email=String(auth.email||"").trim().toLowerCase(); let profiles=[]; if(auth.id){ const byAuth=await db.from("teacher_profiles") .select("id,teacher_name,email,auth_user_id,account_status") .eq("auth_user_id",auth.id); if(!byAuth.error) profiles=byAuth.data||[]; } if(!profiles.length && email){ const byEmail=await db.from("teacher_profiles") .select("id,teacher_name,email,auth_user_id,account_status") .ilike("email",email); if(!byEmail.error) profiles=byEmail.data||[]; } if(!profiles.length) return null; // Find the profile's assignment for the ACTIVE School Year only. for(const profile of profiles){ const ar=await db.from("teacher_assignments") .select("id,grade_level,role,teacher_id,school_year_id,section_id,school_sections(section_name)") .eq("school_year_id",sy.data.id) .eq("teacher_id",profile.id) .order("id",{ascending:false}); if(ar.error) continue; const a=(ar.data||[]).find(x=>x.school_sections?.section_name); if(!a) continue; const assignment={ teacherId:a.teacher_id, teacherName:profile.teacher_name||"", email:profile.email||email, grade:String(a.grade_level||"").trim(), section:String(a.school_sections.section_name||"").trim(), role:a.role||"Adviser", schoolYearId:sy.data.id, schoolYear:sy.data.school_year }; state.currentTeacherAssignment=assignment; window.__PES_SF2_ASSIGNMENT=assignment; window.__PES_ADVISER_ASSIGNMENT_DEBUG={authEmail:email,profileId:profile.id,assignmentId:a.id,grade:assignment.grade,section:assignment.section}; return assignment; } return null; }catch(e){ console.warn("Adviser assignment lookup failed:",e); return null; } } async function refreshSF2Access(){ const gradeEl=document.getElementById("sf2Grade"), sectionEl=document.getElementById("sf2Section"); if(!gradeEl||!sectionEl)return; // Preserve the user's current selection. Re-loading the Grade/Section lists // must never silently jump back to the first Grade or first Section. const previousGrade=String(gradeEl.value||"").trim(); const previousSection=String(sectionEl.value||"").trim(); // Adviser accounts remain locked to their own active assignment. if(state.user==="adviser"){ const a=await loadCurrentAdviserAssignment(); if(!a){ gradeEl.innerHTML=''; sectionEl.innerHTML=''; gradeEl.disabled=true; sectionEl.disabled=true; return; } gradeEl.innerHTML=``; sectionEl.innerHTML=``; gradeEl.value=a.grade; sectionEl.value=a.section; const syInput=document.getElementById("sf2SchoolYear"); if(syInput && a.schoolYear) syInput.value=String(a.schoolYear).replace(/-/g,"–"); gradeEl.disabled=true; sectionEl.disabled=true; return; } // ADMIN: Load the active School Year's real Grade + Section records directly. // Do not depend on the Learners table and do not depend on a nested relationship // query. This is the important fix: all registered sections must appear even when // a section currently has zero learners. gradeEl.disabled=true; sectionEl.disabled=true; gradeEl.innerHTML=''; sectionEl.innerHTML=''; try{ const db=window.__PES_LOGIN_DB || window.PES_QR_DB || (window.supabase?.createClient ? window.supabase.createClient(window.PES_QR_SUPABASE_URL,window.PES_QR_SUPABASE_KEY) : null); if(!db) throw new Error("Supabase database client is unavailable."); const sy=await db.from("school_years").select("id,school_year,is_active") .eq("is_active",true).limit(1).maybeSingle(); if(sy.error) throw sy.error; if(!sy.data) throw new Error("No active School Year is configured."); const rows=[]; const seen=new Set(); // Primary source: school_sections. This contains every registered section, // including sections with no learners yet. const sr=await db.from("school_sections") .select("id,section_name,grade_level") .eq("school_year_id",sy.data.id) .order("grade_level",{ascending:true}) .order("section_name",{ascending:true}); if(!sr.error){ for(const r of (sr.data||[])){ const name=String(r.section_name||"").trim(); if(!name)continue; let grade=String(r.grade_level||"").trim(); // If a section's grade column is blank, recover it from learners assigned // to that section rather than dropping the section from the Admin list. if(!grade){ const learnerGrade=(state.learners||[]).find(l=>String(l.section||"").trim().toLowerCase()===name.toLowerCase() && l.grade); grade=String(learnerGrade?.grade||"").trim(); } const key=name.toLowerCase(); if(!seen.has(key)){ seen.add(key); rows.push({name,grade}); } } } // Secondary source: teacher assignments. This is only used for sections not // already returned by school_sections. const ar=await db.from("teacher_assignments") .select("grade_level,section_id,school_sections(section_name)") .eq("school_year_id",sy.data.id); if(!ar.error){ for(const r of (ar.data||[])){ const name=String(r.school_sections?.section_name||"").trim(); if(!name)continue; const key=name.toLowerCase(); const grade=String(r.grade_level||"").trim(); const existing=rows.find(x=>x.name.toLowerCase()===key); if(existing){ if(!existing.grade && grade)existing.grade=grade; } else { seen.add(key); rows.push({name,grade}); } } } // Final safe fallback: learners can recover a section/grade pair if RLS or a // temporary relationship issue prevents either registration query from filling it. for(const l of (state.learners||[])){ const name=String(l.section||"").trim(); if(!name)continue; const key=name.toLowerCase(); const existing=rows.find(x=>x.name.toLowerCase()===key); if(existing){ if(!existing.grade && l.grade)existing.grade=String(l.grade).trim(); } else rows.push({name,grade:String(l.grade||"").trim()}); } // Keep only real Grade + Section pairs. If a section has no grade anywhere, // it is still shown under the special "Unassigned Grade" group rather than // making the entire Grade dropdown blank. rows.sort((a,b)=>{ const ga=String(a.grade||"Unassigned Grade"), gb=String(b.grade||"Unassigned Grade"); return ga.localeCompare(gb,undefined,{numeric:true}) || a.name.localeCompare(b.name); }); const grades=[...new Set(rows.map(x=>x.grade).filter(Boolean))]; gradeEl.innerHTML=grades.length ? grades.map(g=>``).join("") : ''; const fillSections=(preferredSection="")=>{ const g=String(gradeEl.value||""); const filtered=rows.filter(x=>String(x.grade||"")===g); sectionEl.innerHTML=filtered.length ? filtered.map(x=>``).join("") : ''; sectionEl.disabled=!filtered.length; if(filtered.length){ const wanted=String(preferredSection||"").trim().toLowerCase(); const match=filtered.find(x=>String(x.name).trim().toLowerCase()===wanted); sectionEl.value=match ? match.name : filtered[0].name; } }; gradeEl.onchange=()=>fillSections(""); gradeEl.disabled=!grades.length; // Restore the previous Grade if it is still registered for the active SY. const previousGradeMatch=grades.find(g=>String(g).trim().toLowerCase()===previousGrade.toLowerCase()); gradeEl.value=previousGradeMatch || grades[0] || ""; fillSections(previousSection); const syInput=document.getElementById("sf2SchoolYear"); if(syInput) syInput.value=String(sy.data.school_year||"").replace(/-/g,"–"); window.__PES_SF2_ADMIN_ROWS=rows; window.__PES_SF2_ADMIN_LOAD_DEBUG={schoolYear:sy.data.school_year,rows,schoolSectionsError:sr.error?.message||null,assignmentsError:ar.error?.message||null}; console.log("SF2 Admin Grade/Section options loaded:",window.__PES_SF2_ADMIN_LOAD_DEBUG); }catch(err){ console.error("SF2 Admin Grade/Section loader:",err); gradeEl.innerHTML=''; sectionEl.innerHTML=''; gradeEl.disabled=true; sectionEl.disabled=true; const out=document.getElementById("sf2Preview"); if(out) out.innerHTML='
Unable to load SF2 Grade and Section.
'+escapeHtml(err?.message||String(err))+'
'; } } function installSF2Access(){} document.getElementById("printBtn").onclick=async function(e){ e.preventDefault(); const btn=this; const oldText=btn.textContent; btn.disabled=true; btn.textContent="Preparing Print…"; try{ const preview=document.getElementById("sf2Preview"); const needsGenerate=!preview || !preview.querySelector(".sf2table"); if(needsGenerate){ if(typeof refreshSF2Access==="function") await refreshSF2Access(); generateSF2(); } const report=document.querySelector("#sf2Preview .sf2wrap"); if(!report) throw new Error("Please generate an SF2 preview first."); const w=window.open("","_blank","width=1200,height=900"); if(!w) throw new Error("Please allow pop-ups for the SF2 print window."); w.document.open(); w.document.write(`SF2 Report${report.outerHTML} `); w.document.close(); setTimeout(()=>{w.focus();w.print();},500); }catch(err){ console.error("SF2 print error:",err); alert(err?.message||"Unable to print the SF2 report."); }finally{ btn.disabled=false; btn.textContent=oldText; } }; document.getElementById("saveSettings").onclick=()=>document.getElementById("settingsMsg").innerHTML='
Settings saved for this browser prototype.
'; const uploadLearnersBtn=document.getElementById("uploadLearners"); const learnerFileInput=document.getElementById("learnerFile"); if(uploadLearnersBtn && learnerFileInput){ learnerFileInput.addEventListener("change",function(e){ const file=e.target.files && e.target.files[0]; if(file){ handleLearnerFile(file); } // Allow selecting the same file again after an unsuccessful upload. setTimeout(()=>{e.target.value="";},0); }); } renderDashboard(); renderTeachers();

📅 School Year Management

Manage active and archived school years.
School YearStatusAction
n