Panlaitan QR Attendance

Cross-Platform QR Attendance System · SY 2026–2027 · v1.34

Use the official Supabase Admin account for this QR Attendance System.
`); w.document.close(); } async function downloadAllLearnerQRs(){ let list=getVisibleQRLearners(); if(!list.length){alert("No learners are available for QR download.");return;} if(typeof QRCode==="undefined" || typeof JSZip==="undefined"){ alert("The QR download library is unavailable. Please make sure the device is connected to the internet, then reload the page."); return; } const selected=(document.getElementById("sectionFilter")?.value||"").trim(); const label=selected||((state.user==="adviser"&&state.currentTeacherAssignment)?state.currentTeacherAssignment.section:"All_Sections"); const button=document.getElementById("downloadAllQR"); const original=button?button.textContent:"↓ Download All QR Codes"; if(button){button.disabled=true;button.textContent="Preparing QR Codes...";} try{ const zip=new JSZip(); const folder=zip.folder(`QR_Codes_${label.replace(/[^\w\-]+/g,"_")}`); for(let i=0;isetTimeout(resolve,80)); const qrCanvas=holder.querySelector("canvas"); const qrImg=holder.querySelector("img"); const out=document.createElement("canvas"); out.width=520;out.height=500; const ctx=out.getContext("2d"); ctx.fillStyle="#ffffff";ctx.fillRect(0,0,out.width,out.height); ctx.fillStyle="#000000";ctx.textAlign="center";ctx.textBaseline="middle"; let learnerName=(l.name||"Learner").toUpperCase(); let fontSize=30;if(learnerName.length>30)fontSize=25;if(learnerName.length>42)fontSize=21; ctx.font=`700 ${fontSize}px Arial, sans-serif`;ctx.fillText(learnerName,out.width/2,45); if(qrCanvas) ctx.drawImage(qrCanvas,60,80,400,400); else if(qrImg){ await new Promise(resolve=>{if(qrImg.complete)resolve();else{qrImg.onload=resolve;qrImg.onerror=resolve;}}); ctx.drawImage(qrImg,60,80,400,400); }else{ document.body.removeChild(holder); throw new Error("QR image could not be generated for "+l.name); } document.body.removeChild(holder); const dataUrl=out.toDataURL("image/png"); folder.file(`${String(i+1).padStart(2,"0")}_${(l.name||l.lrn||l.id).replace(/[\\/:*?"<>|]+/g,"_").trim()}.png`,dataUrl.split(",")[1],{base64:true}); } const blob=await zip.generateAsync({type:"blob"}); const url=URL.createObjectURL(blob); const a=document.createElement("a");a.href=url;a.download=`QR_Codes_${label.replace(/[^\w\-]+/g,"_")}.zip`; document.body.appendChild(a);a.click();a.remove(); setTimeout(()=>URL.revokeObjectURL(url),2000); alert(`Downloaded ${list.length} QR code(s). Each PNG contains the learner's name above the QR code.`); }catch(err){ console.error("QR ZIP failed:",err); alert("Unable to create the QR-code ZIP file: "+(err?.message||String(err))); }finally{ if(button){button.disabled=false;button.textContent=original;} } } function printAllLearnerQRs(){ const list=getVisibleQRLearners(); if(!list.length){alert("No learners are available for printing.");return;} const selected=(document.getElementById("sectionFilter")?.value||"").trim(); const label=selected||((state.user==="adviser"&&state.currentTeacherAssignment)?state.currentTeacherAssignment.section:"All Sections"); const cards=list.map((l,i)=>`
PANLAITAN ELEMENTARY SCHOOL
QR Attendance Identification
${String(l.name||"").replace(/&/g,"&").replace(//g,">")}
${String(l.grade||"")} – ${String(l.section||"")}
${String(l.lrn||l.id)}
`).join(""); const qrData=list.map((l,i)=>({i,id:String(l.lrn||l.id)})); const w=window.open("","_blank","width=1000,height=900"); if(!w){alert("Please allow pop-ups to print the QR IDs.");return;} w.document.write(`QR IDs - ${label} `); w.document.close(); } `; 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))+'
'; } } // Expose the SF2 generator for the stable v1.31 handlers and deployed builds. window.generateSF2=generateSF2; window.refreshSF2Access=refreshSF2Access; 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(); const printHtml = `SF2 Report${report.outerHTML} `; w.document.open(); w.document.write(printHtml); 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
`); w.document.close(); return w; } async function downloadAll(){ const list=getList(); if(!list.length){alert("No learners are available for QR download in the selected section.");return;} ensureQRLib(); if(typeof JSZip==="undefined") throw new Error("ZIP download library is unavailable. Please reload the page while connected to the internet."); const button=document.getElementById("downloadAllQR"); const old=button?.textContent||"↓ Download All QR Codes"; if(button){button.disabled=true;button.textContent="Preparing QR Codes…";} try{ const filter=document.getElementById("sectionFilter"); const selected=String(filter?.value||"").trim(); const st=window.state||{}; const label=selected||(st.user==="adviser"?String(st.currentTeacherAssignment?.section||"My_Class"):"All_Sections"); const zip=new JSZip(); const folder=zip.folder("QR_Codes_"+safeName(label).replace(/\s+/g,"_")); for(let i=0;isetTimeout(r,120)); const canvas=holder.querySelector("canvas"); const img=holder.querySelector("img"); const out=document.createElement("canvas"); out.width=520; out.height=500; const ctx=out.getContext("2d"); if(!ctx) throw new Error("Canvas is unavailable on this device."); ctx.fillStyle="#ffffff";ctx.fillRect(0,0,520,500); ctx.fillStyle="#000000";ctx.textAlign="center";ctx.textBaseline="middle"; let name=String(l.name||"Learner").toUpperCase(); let fs=30;if(name.length>30)fs=25;if(name.length>42)fs=21; ctx.font=`700 ${fs}px Arial, sans-serif`;ctx.fillText(name,260,45); if(canvas) ctx.drawImage(canvas,60,80,400,400); else if(img){await new Promise((resolve,reject)=>{if(img.complete&&img.naturalWidth>0)resolve();else{img.onload=resolve;img.onerror=()=>reject(new Error("QR image failed to load."));}});ctx.drawImage(img,60,80,400,400);} else throw new Error("QR image could not be generated for "+name); const data=out.toDataURL("image/png").split(",")[1]; folder.file(String(i+1).padStart(2,"0")+"_"+safeName(l.name||l.lrn||l.id)+".png",data,{base64:true}); }finally{holder.remove();} } const blob=await zip.generateAsync({type:"blob"}); const url=URL.createObjectURL(blob); const a=document.createElement("a");a.href=url;a.download="QR_Codes_"+safeName(label).replace(/\s+/g,"_")+".zip";a.style.display="none"; document.body.appendChild(a);a.click();a.remove(); setTimeout(()=>URL.revokeObjectURL(url),5000); alert(`QR download complete. ${list.length} QR code(s) were included in the ZIP file.`); }finally{ if(button){button.disabled=false;button.textContent=old;} } } function printAll(){ const list=getList(); if(!list.length){alert("No learners are available for printing in the selected section.");return;} const filter=document.getElementById("sectionFilter"); const st=window.state||{}; const label=String(filter?.value||"").trim()||(st.user==="adviser"?String(st.currentTeacherAssignment?.section||"My Class"):"All Sections"); try{ensureQRLib();makePrintWindow(list,label);}catch(e){console.error("PES QR print:",e);alert(e?.message||String(e));} } window.PES_printAllQRCodes=printAll; window.PES_downloadAllQRCodes=async function(){try{await downloadAll();}catch(e){console.error("PES QR download:",e);alert(e?.message||String(e));}}; function bind(){ const p=document.getElementById("printAllQR"),d=document.getElementById("downloadAllQR"); if(p){p.onclick=null;p.addEventListener("click",function(e){e.preventDefault();e.stopPropagation();printAll();});} if(d){d.onclick=null;d.addEventListener("click",function(e){e.preventDefault();e.stopPropagation();window.PES_downloadAllQRCodes();});} } if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",bind);else bind(); setTimeout(bind,300); setTimeout(bind,1000); })(); '); w.document.close(); setTimeout(()=>{try{w.focus();w.print();}catch(_){}},500); }catch(err){console.error('SF2 definitive print error:',err);alert(err&&err.message||String(err));} finally{p.disabled=false;p.textContent=old;} },true); } } if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',bind,{once:true});else bind(); [100,500,1000,2000,4000].forEach(ms=>setTimeout(bind,ms)); })();