Long time learning retention technique

long time learning retention technique

Last Updated:

Written by:

Time to read:

Advertisements

Learning retention is a person’s ability to transfer new information into their long-term memory so that it is easy for them to recall and put that knowledge to use in the future. In simpler words, learning retention is all about making new knowledge stick for a long time.

4 Factors of Learning Retention

Here are a few factors that affect learning retention for an individual.

  1. Interest and motivation
    The interest and motivation of a learner behind a learning program are of uppermost importance. When learning is accompanied by a motive, it is often retained for a long time because the human brain tends to focus more on matters of interest. For example, a sales rep would be more motivated to learn about a CRM than a marketing automation tool. If trained in both, the seller is more likely to retain the CRM tool knowledge for a longer time compared to the automation tool knowledge.
  2. Repetition
    Repetition of the learning material plays an essential role in learning retention. The more an individual repeats or practices a task, the better it is retained in their memory. For example, it’s hard for most kids to learn mathematical times tables. This is why they are advised to write the tables on paper and stick them to the study board to revise and repeat throughout the day.
  3. Association
    Paying attention to the meaning and significance of the content or associating it with real-life scenarios helps individuals learn quickly and retain the information longer.
  4. Use of multiple channels
    Different people prefer different learning styles – some are visual learners, some need hands-on experience, some require an instructor to guide them, etc. Therefore, it’s important to choose the appropriate learning method or technique to boost learning retention for an individual.

What Is the Learning Retention Pyramid?

learning retention pyramid

The learning pyramid, also known as the “cone of learning,” was developed by the National Training Laboratory during the 1960s. It is a theoretical model that illustrates various learning methods of learning along with their expected retention percentage. The pyramid illustrates how well one can retain information based on the different techniques such as listening to a lecture, reading a book, watching videos, etc.

Learning pyramid

  • Reading – In comparison to a lecture, reading is marginally more effective when it comes to learning retention. The advantage of reading over listening to a lecture is that it provides the learner with reference material to recall the information.
  • Audio/visual – Audio and visual learning content make it easy for learners to absorb information. Learning a new skill by watching a quick video is more convenient and less time-consuming than reading lengthy, text-heavy documents.
  • Demonstration – Learning by demonstration involves a teacher or mentor showing the learner how to perform a task by walking them through a step-by-step process. The demonstration provides information more clearly than passive study methods and helps understand and retain complicated details better.
  • Discussion – Discussions offer an active, cooperative learning environment that leads to greater retention of information. Discussions stimulate a learner’s thinking and increase participation and engagement.
  • Practice by doing – Getting “hands-on” experience is one of the most effective learning methods. This learning style allows learners to apply their knowledge to practice every day, which helps them retain the information long-term. According to the 70-20-10 model, 70% of learning comes from employee experiences.
  • Teaching others – According to the learning pyramid, teaching others is the most effective way to master a subject and retain knowledge for a long time. If one can accurately and correctly teach a subject to others, they’ll have a good mastery of the concepts and superior knowledge retention.
`;receiptWindow.document.open(); receiptWindow.document.write(receiptHtml); receiptWindow.document.close(); };function announcementReadByMe(announcementId){ return announcementReads.some( row=> row.announcement_id===announcementId && row.user_id===me.id ); }function announcementReadCount(announcementId){ return announcementReads.filter( row=>row.announcement_id===announcementId ).length; }function announcementAudienceLabel(a){ if(a.audience_type==="all"){ return "Entire academy"; }if(a.audience_type==="group"){ const group=academicGroups.find(g=>g.id===a.group_id); return group?.name||"Academic Group"; }const count=announcementRecipients.filter( row=>row.announcement_id===a.id ).length;return `${count} selected user${count===1?"":"s"}`; }function announcementIsCurrent(a){ const now=Date.now();if( a.starts_at && new Date(a.starts_at).getTime()>now ){ return false; }if( a.expires_at && new Date(a.expires_at).getTime()<=now ){ return false; }return true; }function canManageAnnouncement(a){ return ( me.role==="admin" || ( me.role==="teacher" && a.created_by===me.id ) ); }function unreadAnnouncementCount(){ return announcements.filter( a=> a.is_published!==false && announcementIsCurrent(a) && !announcementReadByMe(a.id) && !canManageAnnouncement(a) ).length; }function renderAnnouncements(main){ const canCreate= me.role==="admin" || me.role==="teacher";const priorityRank={ urgent:3, important:2, normal:1 };const sorted=[ ...announcements ].sort((a,b)=>{ const p= (priorityRank[b.priority]||0)- (priorityRank[a.priority]||0);if(p)return p;return String(b.created_at) .localeCompare(String(a.created_at)); });main.innerHTML=`
Announcements
Academy notices, class updates and important reminders.
${canCreate?` `:""}
${sorted.length?`
${sorted.map(a=>{ const unread= !announcementReadByMe(a.id) && !canManageAnnouncement(a);const scheduled= a.starts_at && new Date(a.starts_at).getTime()>Date.now();const expired= a.expires_at && new Date(a.expires_at).getTime()<=Date.now();return `
${unread?``:""} ${esc(a.title)}
${formatDateTime(a.created_at)}
${esc(a.priority)}
${esc(a.message)}
${esc(announcementAudienceLabel(a))} ${a.is_published===false?` Draft `:""}${scheduled?` Scheduled `:""}${expired?` Expired `:""}
${canManageAnnouncement(a)?`
${announcementReadCount(a.id)} read
`:""}
`; }).join("")}
` :`
No announcements yet.
` } `;const addBtn=main.querySelector("#uaAddAnnouncement"); if(addBtn){ addBtn.onclick=openCreateAnnouncement; } }function teacherAnnouncementGroups(){ if(me.role==="admin"){ return academicGroups.filter( g=>g.is_active!==false ); }const groupIds=new Set( academicGroupMembers .filter( m=> m.user_id===me.id && m.membership_role==="teacher" ) .map(m=>m.group_id) );return academicGroups.filter( g=> groupIds.has(g.id) && g.is_active!==false ); }function openCreateAnnouncement(){ const groups=teacherAnnouncementGroups();if(me.role==="teacher"&&!groups.length){ return toast( "You are not assigned to an Academic Group." ); }const admin=me.role==="admin";modal("New Announcement",`
${admin?` `:""}
`);const audience=document.getElementById("uaAnnAudience"); const groupWrap=document.getElementById("uaAnnGroupWrap"); const usersWrap=document.getElementById("uaAnnUsersWrap");audience.onchange=()=>{ if(groupWrap){ groupWrap.style.display= audience.value==="group"?"block":"none"; }if(usersWrap){ usersWrap.style.display= audience.value==="users"?"block":"none"; } };document.getElementById("uaSaveAnnouncement").onclick=function(){ createAnnouncement(this); }; }async function createAnnouncement(button){ const title= document.getElementById("uaAnnTitle").value.trim();const message= document.getElementById("uaAnnMessage").value.trim();const priority= document.getElementById("uaAnnPriority").value;const audience= document.getElementById("uaAnnAudience").value;const groupId= audience==="group" ?document.getElementById("uaAnnGroup")?.value||null :null;const url= document.getElementById("uaAnnUrl").value.trim();const startRaw= document.getElementById("uaAnnStart").value;const expiryRaw= document.getElementById("uaAnnExpiry").value;const published= document.getElementById("uaAnnPublished").checked;let userIds=[];if(audience==="users"){ userIds=[ ...document.getElementById("uaAnnUsers").selectedOptions ].map(o=>o.value); }if(!title||!message){ return toast("Title and message are required."); }if(audience==="group"&&!groupId){ return toast("Select an Academic Group."); }if(audience==="users"&&!userIds.length){ return toast("Select at least one user."); }const startsAt= startRaw?new Date(startRaw).toISOString():null;const expiresAt= expiryRaw?new Date(expiryRaw).toISOString():null;if( startsAt && expiresAt && new Date(expiresAt).getTime()<= new Date(startsAt).getTime() ){ return toast("Expiry must be after the start time."); }const restore=setButtonBusy(button,"Saving…"); let announcement=null;try{ const rows=await rest("announcements",{ method:"POST", body:{ title, message, priority, audience_type:audience, group_id:groupId, external_url:url||null, starts_at:startsAt, expires_at:expiresAt, is_published:published, created_by:me.id }, prefer:"return=representation" });announcement=rows?.[0];if(!announcement){ throw new Error( "Announcement could not be created." ); }if(audience==="users"&&userIds.length){ await rest("announcement_recipients",{ method:"POST", body:userIds.map(user_id=>({ announcement_id:announcement.id, user_id })), prefer:"return=minimal" }); }restore(); closeModal(); await refresh(); renderMain("announcements"); toast("Announcement saved."); }catch(e){ if(announcement?.id){ try{ await rest( `announcements?id=eq.${announcement.id}`, { method:"DELETE", prefer:"return=minimal" } ); }catch(_){} }toast(e.message); }finally{ restore(); } }async function markAnnouncementRead(announcementId){ if(announcementReadByMe(announcementId)){ return; }try{ const readAt=new Date().toISOString();await rest( "announcement_reads?on_conflict=announcement_id,user_id", { method:"POST", body:{ announcement_id:announcementId, user_id:me.id, read_at:readAt }, prefer:"resolution=merge-duplicates,return=minimal" } );announcementReads.push({ announcement_id:announcementId, user_id:me.id, read_at:readAt }); }catch(_){ // The notice still opens if read tracking fails. } }window.UAopenAnnouncement=async function(announcementId){ const a=announcements.find( x=>x.id===announcementId );if(!a)return;if(!canManageAnnouncement(a)){ await markAnnouncementRead(a.id); }let body=`
${esc(a.title)}
${formatDateTime(a.created_at)}
${esc(a.priority)}
${esc(a.message)}
${esc(announcementAudienceLabel(a))} ${a.starts_at?` From ${formatDateTime(a.starts_at)} `:""}${a.expires_at?` Until ${formatDateTime(a.expires_at)} `:""}
${a.external_url?`
`:""} `;if(canManageAnnouncement(a)){ body+=`
${announcementReadCount(a.id)} read receipt${announcementReadCount(a.id)===1?"":"s"}
`; }modal("Announcement",body);const linkBtn=document.getElementById("uaAnnOpenLink");if(linkBtn){ linkBtn.onclick=()=>{ window.open( a.external_url, "_blank", "noopener,noreferrer" ); }; } };window.UAtoggleAnnouncementPublish=async function( announcementId, button ){ const a=announcements.find( x=>x.id===announcementId );if(!a||!canManageAnnouncement(a)){ return; }const restore=setButtonBusy( button, a.is_published?"Unpublishing…":"Publishing…" );try{ await rest( `announcements?id=eq.${announcementId}`, { method:"PATCH", body:{ is_published:!a.is_published }, prefer:"return=minimal" } );restore(); closeModal(); await refresh(); renderMain("announcements"); toast( a.is_published ?"Announcement unpublished." :"Announcement published." ); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAdeleteAnnouncement=async function( announcementId, button ){ if(!confirm("Delete this announcement permanently?")){ return; }const restore=setButtonBusy(button,"Deleting…");try{ await rest( `announcements?id=eq.${announcementId}`, { method:"DELETE", prefer:"return=minimal" } );restore(); closeModal(); await refresh(); renderMain("announcements"); toast("Announcement deleted."); }catch(e){ toast(e.message); }finally{ restore(); } };function renderSavedArticles(main){ main.innerHTML=`
Saved Articles
Articles you bookmarked while browsing unipolaris.com.
${savedArticles.length?` ${savedArticles.length} saved `:""}
${savedArticles.length?`
${savedArticles.map(article=>`
${article.article_image_url?` `:""}
${esc(article.article_title)}
${article.article_excerpt?`
${esc(article.article_excerpt)}
`:""}
Saved ${formatDateTime(article.saved_at)}
`).join("")}
` :`
No saved articles yet.

Open an article on unipolaris.com and use the Save article button.
` } `; }window.UAopenSavedArticle=function(encodedUrl){ const url=decodeURIComponent(encodedUrl); window.open(url,"_blank","noopener,noreferrer"); };window.UAremoveSavedArticle=async function(articleId,button){ const restore=setButtonBusy(button,"Removing…");try{ await rest(`saved_articles?id=eq.${articleId}`,{ method:"DELETE", prefer:"return=minimal" });restore(); await refresh(); renderMain("saved"); toast("Article removed."); }catch(e){ toast(e.message); }finally{ restore(); } };function stopAssessmentTimer(){ if(assessmentTimerId){ clearInterval(assessmentTimerId); assessmentTimerId=null; } }function assessmentTypeLabel(type){ return ({ lesson_quiz:"Lesson quiz", chapter_test:"Chapter test", class_test:"Class test", practice_test:"Practice test" })[type]||"Test"; }function assessmentGroupIds(assessmentId){ return assessmentGroups .filter(x=>x.assessment_id===assessmentId) .map(x=>x.group_id); }function questionsForAssessment(assessmentId){ return assessmentQuestions .filter(x=>x.assessment_id===assessmentId) .sort((a,b)=>(a.position||0)-(b.position||0)); }function attemptsForAssessment(assessmentId,studentId=null){ return assessmentAttempts.filter(a=> a.assessment_id===assessmentId && (!studentId||a.student_id===studentId) ); }function assessmentQuestionCount(assessmentId){ return questionsForAssessment(assessmentId).length; }function assessmentMaxMarks(assessmentId){ return questionsForAssessment(assessmentId) .reduce((sum,x)=>sum+Number(x.marks||0),0); }function latestStudentAttempt(assessmentId,studentId=me.id){ return attemptsForAssessment(assessmentId,studentId) .sort((a,b)=>String(b.started_at).localeCompare(String(a.started_at)))[0]||null; }function canManageAssessment(a){ return ( me.role==="admin" || ( me.role==="teacher" && a.created_by===me.id ) ); }function lessonLabel(lessonId){ const lesson=courseLessons.find(l=>l.id===lessonId); if(!lesson)return "Lesson"; const section=courseSections.find(s=>s.id===lesson.section_id); const course=courses.find(c=>c.id===section?.course_id);return [course?.title,section?.title,lesson.title] .filter(Boolean) .join(" · "); }function assessmentTargetLabel(a){ if(a.lesson_id){ return lessonLabel(a.lesson_id); }const labels=assessmentGroupIds(a.id) .map(id=>groupName(id)) .filter(Boolean);return labels.join(", ")||"Academic group"; }function assessmentAvailabilityLabel(a){ const now=Date.now();if(a.available_from && nownew Date(a.available_until).getTime()){ return "Closed"; }return "Available"; }function renderAssessments(main){ const canCreate=me.role==="admin"||me.role==="teacher";main.innerHTML=`
Tests & Quizzes
Lesson quizzes, chapter tests and reusable question bank.
${canCreate?`
`:""}
${assessments.length?`
${assessments.map(a=>{ const latest=isLearner() ?latestStudentAttempt(a.id) :null; const submitted=attemptsForAssessment(a.id) .filter(x=>x.status==="submitted"); const avg=submitted.length ?Math.round( submitted.reduce( (sum,x)=>sum+Number(x.percentage||0), 0 )/submitted.length ) :null;return `
${esc(a.title)}
${esc(assessmentTargetLabel(a))}
${esc(assessmentTypeLabel(a.assessment_type))}
${a.description?`
${esc(a.description)}
`:""}
${assessmentQuestionCount(a.id)} question${assessmentQuestionCount(a.id)===1?"":"s"} ${assessmentMaxMarks(a.id)} marks ${a.time_limit_minutes?` ${a.time_limit_minutes} min `:""}${a.is_published===false?` Draft`:""}
${ isLearner() ?latest?.status==="submitted" ?`Latest: ${Number(latest.percentage||0).toFixed(0)}% · ${latest.passed?"Passed":"Not passed"}` :assessmentAvailabilityLabel(a) :submitted.length ?`${submitted.length} submitted · ${avg}% average` :"No submissions yet" }
`; }).join("")}
` :`
${canCreate ?"No tests or quizzes have been created yet." :"No test or quiz is assigned to your account yet."}
` } `;if(canCreate){ main.querySelector("#uaQuestionBankBtn").onclick=openQuestionBank; main.querySelector("#uaAddAssessment").onclick=openCreateAssessment; } }function manageableLessons(){ return courseLessons.filter(lesson=>{ const section=courseSections.find(s=>s.id===lesson.section_id); const course=courses.find(c=>c.id===section?.course_id); return course && canManageCourse(course); }); }function openCreateAssessment(){ const groups=academicGroups.filter(g=>g.is_active!==false); const lessons=manageableLessons();modal("Create test / quiz",`
If a lesson is selected, access is inherited from that course.
After saving, open the test and add questions from the Question Bank.
`);const typeSelect=document.getElementById("uaAType"); const lessonSelect=document.getElementById("uaALesson");typeSelect.addEventListener("change",()=>{ if(typeSelect.value==="lesson_quiz" && !lessonSelect.value && lessons.length){ lessonSelect.value=lessons[0].id; } });lessonSelect.addEventListener("change",()=>{ if(lessonSelect.value){ typeSelect.value="lesson_quiz"; } });document.getElementById("uaSaveAssessment").onclick=function(){ createAssessment(this); }; }async function createAssessment(button){ const title=document.getElementById("uaATitle").value.trim(); const description=document.getElementById("uaADescription").value.trim(); const type=document.getElementById("uaAType").value; const lessonId=document.getElementById("uaALesson").value||null; const timeRaw=document.getElementById("uaATime").value.trim(); const attemptsRaw=document.getElementById("uaAAttempts").value.trim(); const passRaw=document.getElementById("uaAPass").value.trim(); const published=document.getElementById("uaAPublished").checked; const groupIds=[ ...document.getElementById("uaAGroups").selectedOptions ].map(o=>o.value);if(!title)return toast("Enter a test title.");if(!lessonId&&!groupIds.length){ return toast("Select at least one Academic Group or attach the quiz to a lesson."); }const timeLimit=timeRaw?Number(timeRaw):null; const maxAttempts=attemptsRaw?Number(attemptsRaw):null; const passPercent=passRaw===""?40:Number(passRaw);if(timeLimit!==null&&(!Number.isInteger(timeLimit)||timeLimit<1)){ return toast("Time limit must be a whole number of minutes."); }if(maxAttempts!==null&&(!Number.isInteger(maxAttempts)||maxAttempts<1)){ return toast("Maximum attempts must be a whole number."); }if(!Number.isFinite(passPercent)||passPercent<0||passPercent>100){ return toast("Pass percentage must be between 0 and 100."); }const restore=setButtonBusy(button,"Creating…"); let assessment=null;try{ const rows=await rest("assessments",{ method:"POST", body:{ title, description:description||null, assessment_type:lessonId?"lesson_quiz":type, lesson_id:lessonId, time_limit_minutes:timeLimit, max_attempts:maxAttempts, pass_percent:passPercent, created_by:me.id, is_published:published }, prefer:"return=representation" });assessment=rows?.[0]; if(!assessment){ throw new Error("Test could not be created."); }if(!lessonId&&groupIds.length){ await rest("assessment_groups",{ method:"POST", body:groupIds.map(group_id=>({ assessment_id:assessment.id, group_id })), prefer:"return=minimal" }); }restore(); closeModal(); await refresh(); window.UAopenAssessment(assessment.id); toast("Test created. Add questions now."); }catch(e){ if(assessment?.id){ try{ await rest(`assessments?id=eq.${assessment.id}`,{ method:"DELETE", prefer:"return=minimal" }); }catch(_){} }toast(e.message); }finally{ restore(); } }window.UAopenAssessment=function(assessmentId){ stopAssessmentTimer();const a=assessments.find(x=>x.id===assessmentId); if(!a)return;const manager=canManageAssessment(a); const aq=questionsForAssessment(a.id); const maxMarks=assessmentMaxMarks(a.id);let body=`
Type${esc(assessmentTypeLabel(a.assessment_type))}
Status${a.is_published?"Published":"Draft"}
Questions${aq.length}
Total marks${maxMarks}
Pass${Number(a.pass_percent||0)}%
Time${a.time_limit_minutes?`${a.time_limit_minutes} min`:"No limit"}
${a.description?`
${esc(a.description)}
`:""}
${esc(assessmentTargetLabel(a))}
`;if(manager){ body+=`
`;if(aq.length){ body+=`
Questions ${aq.map((item,index)=>{ const q=questionBank.find(x=>x.id===item.question_id); return `
Q${index+1}. ${esc(q?.prompt||"Question")}
${q?.question_type==="true_false"?"True / False":"MCQ"} · ${Number(item.marks||0)} mark${Number(item.marks||0)===1?"":"s"}
`; }).join("")}
`; }else{ body+=`
Add at least one question before publishing this test.
`; }const submitted=attemptsForAssessment(a.id) .filter(x=>x.status==="submitted");if(submitted.length){ body+=`
Student results${submitted.map(attempt=>`
${esc(profileName(attempt.student_id))}
Attempt ${attempt.attempt_no} · ${formatDateTime(attempt.submitted_at)}
${Number(attempt.percentage||0).toFixed(0)}%
`).join("")}
`; }body+=`
`; }else{ const myAttempts=attemptsForAssessment(a.id,me.id) .sort((x,y)=>String(y.started_at).localeCompare(String(x.started_at))); const latest=myAttempts[0]||null;if(latest?.status==="submitted"){ body+=`
${Number(latest.percentage||0).toFixed(0)}% ${latest.passed?"Passed":"Not passed"}
${Number(latest.score||0)} / ${Number(latest.max_score||0)} marks
`; }if(aq.length){ body+=`
`; }else{ body+=`
This test has no questions yet.
`; }if(myAttempts.length){ body+=`
My attempts ${myAttempts.map(attempt=>`
Attempt ${attempt.attempt_no} · ${attempt.status==="submitted" ?`${Number(attempt.percentage||0).toFixed(0)}%` :attempt.status.replace("_"," ") }
`).join("")}
`; } }modal("Test / quiz",body);const startBtn=document.getElementById("uaStartAssessment"); if(startBtn){ startBtn.onclick=function(){ startAssessmentAttempt(a.id,this); }; } };function openQuestionBank(){ const manageable=questionBank.filter(q=> me.role==="admin"||q.created_by===me.id );modal("Question bank",`
${manageable.length?manageable.map(q=>`
${esc(q.prompt)}
${q.question_type==="true_false"?"True / False":"MCQ"}
${profileName(q.created_by)}
`).join(""):`
No questions in your Question Bank yet.
`}
`);document.getElementById("uaCreateQuestionBankItem").onclick=()=>{ openCreateQuestion(null); }; }window.UAaddQuestionToAssessment=function(assessmentId){ closeModal(); openCreateQuestion(assessmentId); };function openCreateQuestion(assessmentId=null){ modal( assessmentId?"Create & add question":"Create question", `
${assessmentId?` `:""}
` );const type=document.getElementById("uaQType"); const mcq=document.getElementById("uaMCQOptions"); const tf=document.getElementById("uaTFOptions");type.onchange=()=>{ const isTF=type.value==="true_false"; mcq.style.display=isTF?"none":"block"; tf.style.display=isTF?"block":"none"; };document.getElementById("uaSaveQuestion").onclick=function(){ saveQuestion(assessmentId,this); }; }async function saveQuestion(assessmentId,button){ const type=document.getElementById("uaQType").value; const prompt=document.getElementById("uaQPrompt").value.trim(); const explanation=document.getElementById("uaQExplanation").value.trim();if(!prompt)return toast("Enter the question.");let options=null; let correctAnswer=null;if(type==="mcq_single"){ options=[0,1,2,3].map(i=> document.getElementById(`uaQOpt${i}`).value.trim() );if(options.some(x=>!x)){ return toast("Enter all four MCQ options."); }correctAnswer=Number( document.getElementById("uaQCorrectMCQ").value ); }else{ correctAnswer= document.getElementById("uaQCorrectTF").value==="true"; }let marks=1;if(assessmentId){ marks=Number(document.getElementById("uaQMarks").value||1); if(!Number.isFinite(marks)||marks<=0){ return toast("Marks must be greater than 0."); } }const restore=setButtonBusy(button,"Saving…"); let question=null;try{ const rows=await rest("question_bank",{ method:"POST", body:{ question_type:type, prompt, options, created_by:me.id }, prefer:"return=representation" });question=rows?.[0]; if(!question){ throw new Error("Question could not be created."); }await rest("question_answer_keys",{ method:"POST", body:{ question_id:question.id, correct_answer:correctAnswer, explanation:explanation||null }, prefer:"return=minimal" });if(assessmentId){ const position= Math.max( 0, ...questionsForAssessment(assessmentId) .map(x=>Number(x.position)||0) )+1;await rest("assessment_questions",{ method:"POST", body:{ assessment_id:assessmentId, question_id:question.id, position, marks }, prefer:"return=minimal" }); }restore(); closeModal(); await refresh();if(assessmentId){ window.UAopenAssessment(assessmentId); toast("Question added to test."); }else{ openQuestionBank(); toast("Question saved to Question Bank."); } }catch(e){ if(question?.id){ try{ await rest(`question_bank?id=eq.${question.id}`,{ method:"DELETE", prefer:"return=minimal" }); }catch(_){} }toast(e.message); }finally{ restore(); } }window.UAaddExistingQuestion=function(assessmentId){ const linked=new Set( questionsForAssessment(assessmentId) .map(x=>x.question_id) );const available=questionBank.filter(q=> !linked.has(q.id) && (me.role==="admin"||q.created_by===me.id) );closeModal();modal("Add from Question Bank",` ${available.length?`
` :`
No unused Question Bank item is available.
` } `);const btn=document.getElementById("uaAttachQuestion"); if(btn){ btn.onclick=function(){ attachExistingQuestion(assessmentId,this); }; } };async function attachExistingQuestion(assessmentId,button){ const questionId=document.getElementById("uaExistingQuestion").value; const marks=Number(document.getElementById("uaExistingMarks").value||1);if(!questionId)return toast("Select a question."); if(!Number.isFinite(marks)||marks<=0){ return toast("Marks must be greater than 0."); }const position= Math.max( 0, ...questionsForAssessment(assessmentId) .map(x=>Number(x.position)||0) )+1;const restore=setButtonBusy(button,"Adding…");try{ await rest("assessment_questions",{ method:"POST", body:{ assessment_id:assessmentId, question_id:questionId, position, marks }, prefer:"return=minimal" });restore(); closeModal(); await refresh(); window.UAopenAssessment(assessmentId); toast("Question added."); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAremoveAssessmentQuestion=async function( assessmentId, questionId, button ){ if(!confirm("Remove this question from the test?"))return;const restore=setButtonBusy(button,"Removing…");try{ await rest( `assessment_questions?assessment_id=eq.${assessmentId}&question_id=eq.${questionId}`, { method:"DELETE", prefer:"return=minimal" } );restore(); closeModal(); await refresh(); window.UAopenAssessment(assessmentId); toast("Question removed."); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAtoggleAssessmentPublish=async function(assessmentId,button){ const a=assessments.find(x=>x.id===assessmentId); if(!a||!canManageAssessment(a))return;if(!a.is_published && !assessmentQuestionCount(a.id)){ return toast("Add at least one question before publishing."); }const restore=setButtonBusy( button, a.is_published?"Unpublishing…":"Publishing…" );try{ await rest(`assessments?id=eq.${assessmentId}`,{ method:"PATCH", body:{ is_published:!a.is_published }, prefer:"return=minimal" });restore(); closeModal(); await refresh(); window.UAopenAssessment(assessmentId); toast(a.is_published?"Test unpublished.":"Test published."); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAdeleteAssessment=async function(assessmentId,button){ const a=assessments.find(x=>x.id===assessmentId); if(!a||!canManageAssessment(a))return;if( !confirm( "Delete this test and all student attempts/results?" ) ){ return; }const restore=setButtonBusy(button,"Deleting…");try{ await rest(`assessments?id=eq.${assessmentId}`,{ method:"DELETE", prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("tests"); toast("Test deleted."); }catch(e){ toast(e.message); }finally{ restore(); } };async function startAssessmentAttempt(assessmentId,button){ const restore=setButtonBusy(button,"Starting…");try{ const attemptInfo=await rest( "rpc/start_assessment_attempt", { method:"POST", body:{ p_assessment_id:assessmentId } } );restore(); closeModal(); await refresh(); openAssessmentAttempt(assessmentId,attemptInfo); }catch(e){ toast(e.message); }finally{ restore(); } }function openAssessmentAttempt(assessmentId,attemptInfo){ stopAssessmentTimer();const a=assessments.find(x=>x.id===assessmentId); if(!a)return;const aq=questionsForAssessment(a.id); const qRows=aq.map(item=>({ ...item, question:questionBank.find(q=>q.id===item.question_id) })).filter(x=>x.question);const attemptId=attemptInfo?.attempt_id; const deadline=attemptInfo?.deadline ?new Date(attemptInfo.deadline) :null;let body=` ${deadline?`
Time remaining
`:""}
${esc(a.title)}
Attempt ${attemptInfo?.attempt_no||1} · ${assessmentMaxMarks(a.id)} marks
${qRows.map((item,index)=>{ const q=item.question;return `
Question ${index+1} · ${Number(item.marks||0)} mark${Number(item.marks||0)===1?"":"s"}
${esc(q.prompt)}${ q.question_type==="mcq_single" ?(Array.isArray(q.options)?q.options:[]).map((option,optIndex)=>` `).join("") :` ` }
`; }).join("")}
`;modal("Attempt test",body);const submitBtn=document.getElementById("uaSubmitAssessment");submitBtn.onclick=function(){ if(confirm("Submit this test now?")){ submitAssessmentAttempt( attemptId, assessmentId, this, false ); } };if(deadline){ function updateTimer(){ const el=document.getElementById("uaAssessmentTimer");if(!el){ stopAssessmentTimer(); return; }const remaining=deadline.getTime()-Date.now();if(remaining<=0){ el.textContent="Time is over — submitting…"; stopAssessmentTimer();submitAssessmentAttempt( attemptId, assessmentId, submitBtn, true ); return; }const totalSeconds=Math.ceil(remaining/1000); const mins=Math.floor(totalSeconds/60); const secs=totalSeconds%60;el.textContent= `Time remaining: ${mins}:${String(secs).padStart(2,"0")}`; }updateTimer(); assessmentTimerId=setInterval(updateTimer,1000); } }function collectAssessmentAnswers(){ const rows=[ ...document.querySelectorAll( "#ua-scheduler .ua-question-card[data-question]" ) ];return rows .map(row=>{ const questionId=row.dataset.question; const type=row.dataset.type; const checked=row.querySelector( `input[name="ua_test_${questionId}"]:checked` );if(!checked)return null;let answer;if(type==="true_false"){ answer=checked.value==="true"; }else{ answer=Number(checked.value); }return { question_id:questionId, answer }; }) .filter(Boolean); }async function submitAssessmentAttempt( attemptId, assessmentId, button, automatic=false ){ stopAssessmentTimer();const answers=collectAssessmentAnswers(); const restore=setButtonBusy( button, automatic?"Submitting…":"Submitting…" );try{ const result=await rest( "rpc/submit_assessment_attempt", { method:"POST", body:{ p_attempt_id:attemptId, p_answers:answers } } );restore(); closeModal(); await refresh(); showAssessmentResult(assessmentId,result); }catch(e){ toast(e.message); if(automatic){ closeModal(); await refresh(); renderMain("tests"); } }finally{ restore(); } }function showAssessmentResult(assessmentId,result){ const a=assessments.find(x=>x.id===assessmentId); if(!a)return;const passed=!!result?.passed; const percentage=Number(result?.percentage||0); const score=Number(result?.score||0); const maxScore=Number(result?.max_score||0);modal("Test result",`
${percentage.toFixed(0)}% ${passed?"Passed":"Not passed"}
${score} / ${maxScore} marks
${passed ?`You reached the pass mark of ${Number(a.pass_percent||0)}%.` :`Pass mark: ${Number(a.pass_percent||0)}%.`}
`);document.getElementById("uaBackTests").onclick=()=>{ closeModal(); switchPortalTab("tests"); }; }function enrollmentState(enrollment){ if(!enrollment)return null;if(enrollment.status!=="active"){ return enrollment.status; }const now=Date.now();if( enrollment.access_starts_at && new Date(enrollment.access_starts_at).getTime()>now ){ return "scheduled"; }if( enrollment.access_expires_at && new Date(enrollment.access_expires_at).getTime()<=now ){ return "expired"; }return "active"; }function enrollmentStateLabel(enrollment){ const state=enrollmentState(enrollment);return ({ active:"Active", scheduled:"Scheduled", pending:"Pending", expired:"Expired", cancelled:"Cancelled" })[state]||"Access"; }function isEnrollmentCurrentlyActive(enrollment){ return enrollmentState(enrollment)==="active"; }function courseIndividualEnrollments(courseId){ return courseEnrollments .filter(e=>e.course_id===courseId) .sort( (a,b)=> String(b.enrolled_at||"") .localeCompare(String(a.enrolled_at||"")) ); }function studentCourseEnrollment( courseId, studentId=me?.id ){ return courseEnrollments.find( e=> e.course_id===courseId && e.student_id===studentId )||null; }function childCourseEnrollments(studentId){ return courseEnrollments.filter( e=>e.student_id===studentId ); }function linkedChildrenCourseAccess(){ const childIds=new Set( linkedChildren().map(child=>child.id) );return courseEnrollments.filter( e=>childIds.has(e.student_id) ); }function enrollmentCourseName(enrollment){ return courses.find( c=>c.id===enrollment.course_id )?.title||"Recorded Course"; }function enrollmentAccessText(enrollment){ const state=enrollmentState(enrollment);if(state==="scheduled"){ return `Starts ${formatDateTime(enrollment.access_starts_at)}`; }if(state==="expired"){ return enrollment.access_expires_at ?`Expired ${formatDateTime(enrollment.access_expires_at)}` :"Expired"; }if(state==="cancelled"){ return "Access cancelled"; }if(state==="pending"){ return "Waiting for activation"; }if(enrollment.access_expires_at){ return `Access until ${formatDateTime(enrollment.access_expires_at)}`; }return "No expiry"; }function courseGroupIds(courseId){ return courseGroups .filter(cg=>cg.course_id===courseId) .map(cg=>cg.group_id); }function courseGroupLabels(courseId){ return courseGroupIds(courseId) .map(id=>academicGroups.find(g=>g.id===id)) .filter(Boolean) .map(group=>group.name); }function sectionsForCourse(courseId){ return courseSections .filter(s=>s.course_id===courseId) .sort((a,b)=>(a.position||0)-(b.position||0)); }function lessonsForSection(sectionId){ return courseLessons .filter(l=>l.section_id===sectionId) .sort((a,b)=>(a.position||0)-(b.position||0)); }function lessonsForCourse(courseId){ const sectionIds=sectionsForCourse(courseId).map(s=>s.id); return courseLessons .filter(l=>sectionIds.includes(l.section_id)) .sort((a,b)=>(a.position||0)-(b.position||0)); }function linkedResourcesForLesson(lessonId){ const ids=lessonResources .filter(x=>x.lesson_id===lessonId) .sort((a,b)=>(a.position||0)-(b.position||0)) .map(x=>x.learning_resource_id);return ids .map(id=>learningResources.find(r=>r.id===id)) .filter(Boolean); }function myLessonProgress(lessonId){ return lessonProgress.find( p=>p.lesson_id===lessonId && p.student_id===me.id ); }function courseProgressPercent(courseId){ if(!isLearner())return null;const published=lessonsForCourse(courseId) .filter(l=>l.is_published!==false);if(!published.length)return 0;const completed=published.filter( l=>myLessonProgress(l.id)?.status==="completed" ).length;return Math.round((completed/published.length)*100); }function canManageCourse(course){ return ( me.role==="admin" || ( me.role==="teacher" && course.created_by===me.id ) ); }function courseStudentIds(courseId){ const groupIds=courseGroupIds(courseId); const ids=new Set();academicGroupMembers.forEach(gm=>{ if( groupIds.includes(gm.group_id) && gm.membership_role==="student" ){ ids.add(gm.user_id); } });courseIndividualEnrollments(courseId) .filter(isEnrollmentCurrentlyActive) .forEach(enrollment=>{ ids.add(enrollment.student_id); });return [...ids]; }function courseProgressSummary(courseId){ const studentIds=courseStudentIds(courseId); const lessons=lessonsForCourse(courseId) .filter(l=>l.is_published!==false);if(!studentIds.length||!lessons.length){ return { students:studentIds.length, completedStudents:0 }; }const completedStudents=studentIds.filter(studentId=>{ return lessons.every(lesson=>{ return lessonProgress.some( p=> p.student_id===studentId && p.lesson_id===lesson.id && p.status==="completed" ); }); }).length;return { students:studentIds.length, completedStudents }; }function videoEmbedUrl(rawUrl){ if(!rawUrl)return null;try{ const url=new URL(rawUrl); const host=url.hostname.replace(/^www\./,"").toLowerCase();if(host==="youtu.be"){ const id=url.pathname.replace(/^\/+/,"").split("/")[0]; return id?`https://www.youtube.com/embed/${encodeURIComponent(id)}`:null; }if( host==="youtube.com" || host==="m.youtube.com" ){ if(url.pathname==="/watch"){ const id=url.searchParams.get("v"); return id?`https://www.youtube.com/embed/${encodeURIComponent(id)}`:null; }const match=url.pathname.match(/^\/(?:embed|shorts)\/([^/?#]+)/); if(match?.[1]){ return `https://www.youtube.com/embed/${encodeURIComponent(match[1])}`; } }if(host==="vimeo.com"||host==="player.vimeo.com"){ const parts=url.pathname.split("/").filter(Boolean); const id=parts.reverse().find(x=>/^\d+$/.test(x)); return id?`https://player.vimeo.com/video/${encodeURIComponent(id)}`:null; } }catch(_){}return null; }function renderCourses(main){ const canCreate=me.role==="admin"||me.role==="teacher";main.innerHTML=`
Recorded Courses
Structured chapters, lessons, video, notes and resources.
${canCreate?` `:""}
${courses.length?`
${courses.map(course=>{ const groupLabels=courseGroupLabels(course.id); const lessonCount=lessonsForCourse(course.id) .filter(l=>l.is_published!==false||canManageCourse(course)) .length; const progress=courseProgressPercent(course.id); const summary=isLearner() ?null :courseProgressSummary(course.id);return `
${esc(course.title)}
${esc( [course.class_level,course.subject] .filter(Boolean) .join(" · ") )}
${lessonCount} lesson${lessonCount===1?"":"s"}
${course.description?`
${esc(course.description)}
`:""}
${groupLabels.slice(0,3).map(label=>` ${esc(label)} `).join("")} ${groupLabels.length>3?` +${groupLabels.length-3}`:""}${isLearner()&&studentCourseEnrollment(course.id)?` ${esc(enrollmentStateLabel(studentCourseEnrollment(course.id)))} `:""}${course.is_published===false?` Draft`:""}
${isLearner()&&studentCourseEnrollment(course.id)?`
${esc(enrollmentAccessText(studentCourseEnrollment(course.id)))}
`:""}${isLearner()?`
${progress}% complete
`:`
${summary.completedStudents}/${summary.students} students completed
` }
`; }).join("")}
` :`
${canCreate ?"No recorded course has been created yet." :"No course is assigned to your account yet."}
` } `;if(canCreate){ main.querySelector("#uaAddCourse").onclick=openAddCourse; } }function openAddCourse(){ const groups=academicGroups.filter(g=>g.is_active!==false);if(!groups.length){ return toast("Create or join an Academic Group first."); }modal("Create recorded course",`
You can create sections and lessons after the course is saved.
`);document.getElementById("uaCreateCourse").onclick=function(){ createCourse(this); }; }async function createCourse(button){ const title=document.getElementById("uaCourseTitle").value.trim(); const description=document.getElementById("uaCourseDescription").value.trim(); const classLevel=document.getElementById("uaCourseLevel").value.trim(); const subject=document.getElementById("uaCourseSubject").value.trim(); const published=document.getElementById("uaCoursePublished").checked; const groupIds=[ ...document.getElementById("uaCourseGroups").selectedOptions ].map(o=>o.value);if(!title){ return toast("Enter a course title."); }if(!groupIds.length){ return toast("Select at least one Academic Group."); }const restore=setButtonBusy(button,"Creating…"); let course=null;try{ const rows=await rest("courses",{ method:"POST", body:{ title, description:description||null, class_level:classLevel||null, subject:subject||null, created_by:me.id, is_published:published }, prefer:"return=representation" });course=rows?.[0]; if(!course)throw new Error("Course could not be created.");await rest("course_groups",{ method:"POST", body:groupIds.map(group_id=>({ course_id:course.id, group_id })), prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("courses"); toast("Course created."); }catch(e){ if(course?.id){ try{ await rest(`courses?id=eq.${course.id}`,{ method:"DELETE", prefer:"return=minimal" }); }catch(_){} }toast(e.message); }finally{ restore(); } }window.UAopenCourse=function(courseId){ const course=courses.find(c=>c.id===courseId); if(!course)return;const manager=canManageCourse(course); const sections=sectionsForCourse(courseId);let body=`
Course ${esc(course.title)}
Status ${course.is_published?"Published":"Draft"}
Class ${esc(course.class_level||"—")}
Subject ${esc(course.subject||"—")}
${course.description?`
${esc(course.description)}
`:""}
${courseGroupLabels(course.id).map(label=>` ${esc(label)} `).join("")}
${isLearner()?`
${courseProgressPercent(course.id)}% complete
`:""} `;if(manager){ body+=`
`;const individualEnrollments= courseIndividualEnrollments(course.id);body+=`
Individual course access${individualEnrollments.length ?individualEnrollments.map(enrollment=>{ const student=profiles.find( p=>p.id===enrollment.student_id );const state=enrollmentState(enrollment);return `
${esc(student?.full_name||student?.email||"Student")}
${esc(student?.login_id||student?.email||"")}
${esc(enrollmentAccessText(enrollment))} · ${esc(enrollment.enrollment_source)} ${enrollment.source_reference ?` · ${esc(enrollment.source_reference)}` :""}
${esc(enrollmentStateLabel(enrollment))} ${state==="active"||state==="scheduled"||state==="pending"?` `:""}
`; }).join("") :`
No individual enrollment yet. Academic Group access still works independently.
` }
`; }if(sections.length){ body+=sections.map(section=>{ const lessons=lessonsForSection(section.id) .filter(l=>l.is_published!==false||manager);return `
${esc(section.title)}
${lessons.length} lesson${lessons.length===1?"":"s"}
${manager?` `:""}
${lessons.length?lessons.map(lesson=>{ const progress=isLearner() ?myLessonProgress(lesson.id) :null;return `
${progress?.status==="completed"?"✅ ":""} ${esc(lesson.title)}
${lesson.video_url?"Video · ":""} ${lesson.estimated_minutes?`${lesson.estimated_minutes} min`:""} ${lesson.is_published===false?" · Draft":""}
Open
`; }).join(""):`
No lessons in this section yet.
`}
`; }).join(""); }else{ body+=`
No sections have been created yet.
`; }if(manager){ body+=`
`; }modal("Recorded course",body); };window.UAenrollCourseStudent=function(courseId){ const course=courses.find(c=>c.id===courseId); if(!course||!canManageCourse(course))return;const students=profiles.filter(isStudentProfile);if(!students.length){ return toast("No Student account is available."); }closeModal();modal("Enroll student in course",`
${esc(course.title)}
This grants access only to this recorded course. It does not add the Student to live classes, Homework or Attendance.
Leave blank to start immediately.
Leave blank for no expiry.
`);document.getElementById("uaSaveEnrollment").onclick=function(){ saveCourseEnrollment(courseId,this); }; };async function saveCourseEnrollment(courseId,button){ const studentId= document.getElementById("uaEnrollStudent").value;const source= document.getElementById("uaEnrollSource").value;const startRaw= document.getElementById("uaEnrollStart").value;const expiryRaw= document.getElementById("uaEnrollExpiry").value;const reference= document.getElementById("uaEnrollReference").value.trim();if(!studentId){ return toast("Select a Student."); }const startAt=startRaw ?new Date(startRaw).toISOString() :new Date().toISOString();const expiresAt=expiryRaw ?new Date(expiryRaw).toISOString() :null;if( expiresAt && new Date(expiresAt).getTime()<= new Date(startAt).getTime() ){ return toast("Access expiry must be after the start time."); }const restore=setButtonBusy(button,"Granting…");try{ await rest( "course_enrollments?on_conflict=course_id,student_id", { method:"POST", body:{ course_id:courseId, student_id:studentId, status:"active", enrollment_source:source, access_starts_at:startAt, access_expires_at:expiresAt, source_reference:reference||null, enrolled_by:me.id }, prefer:"resolution=merge-duplicates,return=minimal" } );restore(); closeModal(); await refresh(); window.UAopenCourse(courseId); toast("Course access granted."); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAcancelCourseEnrollment=async function( enrollmentId, button ){ const enrollment=courseEnrollments.find( e=>e.id===enrollmentId );if(!enrollment)return;if( !confirm( "Cancel this Student's individual course access?" ) ){ return; }const restore=setButtonBusy(button,"Cancelling…");try{ await rest( `course_enrollments?id=eq.${enrollmentId}`, { method:"PATCH", body:{ status:"cancelled" }, prefer:"return=minimal" } );restore(); closeModal(); await refresh(); window.UAopenCourse(enrollment.course_id); toast("Course access cancelled."); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAaddCourseSection=function(courseId){ const course=courses.find(c=>c.id===courseId); if(!course||!canManageCourse(course))return;closeModal();modal("Add course section",`
`);document.getElementById("uaSaveSection").onclick=function(){ saveCourseSection(courseId,this); }; };async function saveCourseSection(courseId,button){ const title=document.getElementById("uaSectionTitle").value.trim();if(!title)return toast("Enter a section title.");const position= Math.max( 0, ...sectionsForCourse(courseId).map(s=>Number(s.position)||0) )+1;const restore=setButtonBusy(button,"Adding…");try{ await rest("course_sections",{ method:"POST", body:{ course_id:courseId, title, position }, prefer:"return=minimal" });restore(); closeModal(); await refresh(); window.UAopenCourse(courseId); toast("Section added."); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAaddCourseLesson=function(sectionId){ const section=courseSections.find(s=>s.id===sectionId); if(!section)return;const course=courses.find(c=>c.id===section.course_id); if(!course||!canManageCourse(course))return;const allowedGroupIds=courseGroupIds(course.id); const availableResources=learningResources.filter( r=> allowedGroupIds.includes(r.group_id) && r.is_published!==false );closeModal();modal("Add lesson",`
YouTube and Vimeo links are embedded automatically.
`);document.getElementById("uaSaveLesson").onclick=function(){ saveCourseLesson(sectionId,this); }; };async function saveCourseLesson(sectionId,button){ const section=courseSections.find(s=>s.id===sectionId); if(!section)return;const title=document.getElementById("uaLessonTitle").value.trim(); const summary=document.getElementById("uaLessonSummary").value.trim(); const contentText=document.getElementById("uaLessonText").value.trim(); const videoUrl=document.getElementById("uaLessonVideo").value.trim(); const minutesRaw=document.getElementById("uaLessonMinutes").value.trim(); const published=document.getElementById("uaLessonPublished").checked;const resourceSelect=document.getElementById("uaLessonResources"); const resourceIds=resourceSelect ?[...resourceSelect.selectedOptions] .filter(o=>!o.disabled) .map(o=>o.value) :[];if(!title)return toast("Enter a lesson title.");if(videoUrl){ try{ const parsed=new URL(videoUrl); if(!["http:","https:"].includes(parsed.protocol)){ throw new Error(); } }catch{ return toast("Enter a valid video URL."); } }const estimatedMinutes=minutesRaw?Number(minutesRaw):null;if( estimatedMinutes!==null && (!Number.isInteger(estimatedMinutes)||estimatedMinutes<1) ){ return toast("Estimated duration must be a whole number of minutes."); }const position= Math.max( 0, ...lessonsForSection(sectionId).map(l=>Number(l.position)||0) )+1;const restore=setButtonBusy(button,"Adding…"); let lesson=null;try{ const rows=await rest("course_lessons",{ method:"POST", body:{ section_id:sectionId, title, summary:summary||null, content_text:contentText||null, video_url:videoUrl||null, estimated_minutes:estimatedMinutes, position, is_published:published }, prefer:"return=representation" });lesson=rows?.[0]; if(!lesson)throw new Error("Lesson could not be created.");if(resourceIds.length){ await rest("lesson_resources",{ method:"POST", body:resourceIds.map((learning_resource_id,index)=>({ lesson_id:lesson.id, learning_resource_id, position:index+1 })), prefer:"return=minimal" }); }restore(); closeModal(); await refresh(); window.UAopenCourse(section.course_id); toast("Lesson added."); }catch(e){ if(lesson?.id){ try{ await rest(`course_lessons?id=eq.${lesson.id}`,{ method:"DELETE", prefer:"return=minimal" }); }catch(_){} }toast(e.message); }finally{ restore(); } }window.UAopenLesson=function(lessonId){ const lesson=courseLessons.find(l=>l.id===lessonId); if(!lesson)return;const section=courseSections.find(s=>s.id===lesson.section_id); const course=courses.find(c=>c.id===section?.course_id);if(!section||!course)return;const manager=canManageCourse(course); const resources=linkedResourcesForLesson(lesson.id); const embed=videoEmbedUrl(lesson.video_url); const progress=isLearner() ?myLessonProgress(lesson.id) :null;let body=`
${esc(course.title)} · ${esc(section.title)}

${esc(lesson.title)}

${lesson.summary?`
${esc(lesson.summary)}
`:""}${embed?`
`:lesson.video_url?`
Recorded video
${esc(lesson.video_url)}
`:""}${lesson.content_text?`
${esc(lesson.content_text)}
`:""}${resources.length?`
Lesson resources${resources.map(resource=>`
${esc(resource.title)}
${esc(resourceKind(resource))}
`).join("")}
`:""}${lesson.estimated_minutes?`
Estimated time: ${lesson.estimated_minutes} minutes
`:""} `;if(isLearner()){ body+=`
`; }if(manager){ body+=`
`; }modal("Course lesson",body);const completeButton=document.getElementById("uaCompleteLesson"); if(completeButton&&!completeButton.disabled){ completeButton.onclick=function(){ markLessonComplete(lesson.id,this); }; } };window.UAcreateLessonQuiz=function(lessonId){ const lesson=courseLessons.find(l=>l.id===lessonId); if(!lesson)return;closeModal(); switchPortalTab("tests");setTimeout(()=>{ openCreateAssessment(); const lessonSelect=document.getElementById("uaALesson"); const typeSelect=document.getElementById("uaAType"); const titleInput=document.getElementById("uaATitle");if(lessonSelect){ lessonSelect.value=lessonId; } if(typeSelect){ typeSelect.value="lesson_quiz"; } if(titleInput&&!titleInput.value){ titleInput.value=`${lesson.title} — Quiz`; } },50); };window.UAopenCourseVideo=function(encodedUrl){ window.open( decodeURIComponent(encodedUrl), "_blank", "noopener,noreferrer" ); };async function markLessonComplete(lessonId,button){ const restore=setButtonBusy(button,"Saving…");try{ const now=new Date().toISOString();await rest( "lesson_progress?on_conflict=lesson_id,student_id", { method:"POST", body:{ lesson_id:lessonId, student_id:me.id, status:"completed", completed_at:now, updated_at:now }, prefer:"resolution=merge-duplicates,return=minimal" } );restore(); closeModal(); await refresh();const lesson=courseLessons.find(l=>l.id===lessonId); const section=courseSections.find(s=>s.id===lesson?.section_id);if(section){ window.UAopenCourse(section.course_id); }else{ renderMain("courses"); }toast("Lesson marked complete."); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAtoggleCoursePublish=async function(courseId,button){ const course=courses.find(c=>c.id===courseId); if(!course||!canManageCourse(course))return;const restore=setButtonBusy( button, course.is_published?"Unpublishing…":"Publishing…" );try{ await rest(`courses?id=eq.${courseId}`,{ method:"PATCH", body:{ is_published:!course.is_published }, prefer:"return=minimal" });restore(); closeModal(); await refresh(); window.UAopenCourse(courseId); toast(course.is_published?"Course unpublished.":"Course published."); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAtoggleLessonPublish=async function(lessonId,button){ const lesson=courseLessons.find(l=>l.id===lessonId); if(!lesson)return;const section=courseSections.find(s=>s.id===lesson.section_id); const course=courses.find(c=>c.id===section?.course_id);if(!course||!canManageCourse(course))return;const restore=setButtonBusy( button, lesson.is_published?"Unpublishing…":"Publishing…" );try{ await rest(`course_lessons?id=eq.${lessonId}`,{ method:"PATCH", body:{ is_published:!lesson.is_published }, prefer:"return=minimal" });restore(); closeModal(); await refresh(); window.UAopenCourse(course.id); toast(lesson.is_published?"Lesson unpublished.":"Lesson published."); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAdeleteLesson=async function(lessonId,button){ const lesson=courseLessons.find(l=>l.id===lessonId); if(!lesson)return;const section=courseSections.find(s=>s.id===lesson.section_id); const course=courses.find(c=>c.id===section?.course_id);if(!course||!canManageCourse(course))return; if(!confirm("Delete this lesson and its progress records?"))return;const restore=setButtonBusy(button,"Deleting…");try{ await rest(`course_lessons?id=eq.${lessonId}`,{ method:"DELETE", prefer:"return=minimal" });restore(); closeModal(); await refresh(); window.UAopenCourse(course.id); toast("Lesson deleted."); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAdeleteCourse=async function(courseId,button){ const course=courses.find(c=>c.id===courseId); if(!course||!canManageCourse(course))return;if( !confirm( "Delete this complete course, all lessons and student progress?" ) ){ return; }const restore=setButtonBusy(button,"Deleting…");try{ await rest(`courses?id=eq.${courseId}`,{ method:"DELETE", prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("courses"); toast("Course deleted."); }catch(e){ toast(e.message); }finally{ restore(); } };async function resourceStorageUpload(path,file){ const s=await sessionReady(); if(!s?.access_token)throw new Error("Please login again.");const ctl=new AbortController(); const timer=setTimeout(()=>ctl.abort(),45000);try{ const r=await fetch( `${CONFIG.SUPABASE_URL}/storage/v1/object/learning-resources/${path.split("/").map(encodeURIComponent).join("/")}`, { method:"POST", signal:ctl.signal, headers:{ "apikey":CONFIG.SUPABASE_PUBLISHABLE_KEY, "Authorization":`Bearer ${s.access_token}`, "Content-Type":file.type||"application/octet-stream", "x-upsert":"false" }, body:file } );const body=await r.json().catch(()=>({}));if(!r.ok){ throw new Error( body?.message|| body?.error|| `File upload failed (${r.status})` ); }return body; }finally{ clearTimeout(timer); } }async function resourceStorageDownload(path,fileName,button){ const restore=setButtonBusy(button,"Opening…");try{ const s=await sessionReady(); if(!s?.access_token)throw new Error("Please login again.");const r=await fetch( `${CONFIG.SUPABASE_URL}/storage/v1/object/authenticated/learning-resources/${path.split("/").map(encodeURIComponent).join("/")}`, { headers:{ "apikey":CONFIG.SUPABASE_PUBLISHABLE_KEY, "Authorization":`Bearer ${s.access_token}` } } );if(!r.ok){ const body=await r.json().catch(()=>({})); throw new Error( body?.message|| body?.error|| `Could not open file (${r.status})` ); }const blob=await r.blob(); const url=URL.createObjectURL(blob); const a=document.createElement("a");a.href=url; a.download=fileName||"study-material"; document.body.appendChild(a); a.click(); a.remove();setTimeout(()=>URL.revokeObjectURL(url),1500); }catch(e){ toast(e.message); }finally{ restore(); } }function validResourceFile(file){ if(!file)return true;const allowedExt=[ "pdf","jpg","jpeg","png","docx","pptx","xlsx" ];const ext=(file.name.split(".").pop()||"").toLowerCase();if(file.size>20*1024*1024){ toast("Study material file must be 20 MB or smaller."); return false; }if(!allowedExt.includes(ext)){ toast("Allowed files: PDF, JPG, PNG, DOCX, PPTX or XLSX."); return false; }return true; }function resourceKind(resource){ const kinds=[]; if(resource.file_path)kinds.push("File"); if(resource.external_url)kinds.push("Link"); if(resource.content_text)kinds.push("Note"); return kinds.length?kinds.join(" + "):"Material"; }function canManageResource(resource){ return ( me.role==="admin" || ( me.role==="teacher" && resource.created_by===me.id ) ); }function renderStudyMaterial(main){ const canCreate=me.role==="admin"||me.role==="teacher";const materials=[...learningResources] .filter(r=>r.is_published!==false || canManageResource(r)) .sort((a,b)=>String(b.created_at).localeCompare(String(a.created_at)));main.innerHTML=`
Study Material
Notes, PDFs, images, worksheets, presentations and useful links.
${canCreate?` `:""}
${materials.length?`
${materials.map(resource=>`
${esc(resource.title)}
${esc(groupName(resource.group_id))}
${esc(resourceKind(resource))}
${resource.description?`
${esc(resource.description)}
`:""}
`).join("")}
` :`
No study material has been added yet.
` } `;if(canCreate){ main.querySelector("#uaAddResource").onclick=openAddResource; } }function openAddResource(){ const groups=academicGroups.filter(g=>g.is_active!==false);if(!groups.length){ return toast("No academic group is available."); }modal("Add study material",`
PDF, JPG, PNG, DOCX, PPTX or XLSX · maximum 20 MB
`);document.getElementById("uaCreateResource").onclick=function(){ createResource(this); }; }async function createResource(button){ const groupId=document.getElementById("uaRGroup").value; const title=document.getElementById("uaRTitle").value.trim(); const description=document.getElementById("uaRDescription").value.trim(); const contentText=document.getElementById("uaRText").value.trim(); const externalUrl=document.getElementById("uaRUrl").value.trim(); const file=document.getElementById("uaRFile").files?.[0]||null; const published=document.getElementById("uaRPublished").checked;if(!groupId||!title){ return toast("Academic group and title are required."); }if(!contentText&&!externalUrl&&!file){ return toast("Add text, an external link or a file."); }if(externalUrl){ try{ const parsed=new URL(externalUrl); if(!["http:","https:"].includes(parsed.protocol)){ throw new Error(); } }catch{ return toast("Enter a valid https:// or http:// link."); } }if(file&&!validResourceFile(file))return;const restore=setButtonBusy(button,"Saving…"); let resource=null;try{ const rows=await rest("learning_resources",{ method:"POST", body:{ group_id:groupId, title, description:description||null, content_text:contentText||null, external_url:externalUrl||null, created_by:me.id, is_published:published }, prefer:"return=representation" });resource=rows?.[0]; if(!resource){ throw new Error("Study material could not be created."); }if(file){ const path= `resources/${resource.id}/${crypto.randomUUID()}-${safeFileName(file.name)}`;await resourceStorageUpload(path,file);await rest(`learning_resources?id=eq.${resource.id}`,{ method:"PATCH", body:{ file_path:path, file_name:file.name, file_type:file.type||null, file_size:file.size }, prefer:"return=minimal" }); }restore(); closeModal(); await refresh(); renderMain("materials"); toast("Study material saved."); }catch(e){ if(resource?.id){ try{ await rest(`learning_resources?id=eq.${resource.id}`,{ method:"DELETE", prefer:"return=minimal" }); }catch(_){} }toast(e.message); }finally{ restore(); } }window.UAopenResource=function(resourceId){ const resource=learningResources.find(r=>r.id===resourceId); if(!resource)return;let body=`
Academic group${esc(groupName(resource.group_id))}
Type${esc(resourceKind(resource))}
Added by${esc(profileName(resource.created_by))}
Added${formatDateTime(resource.created_at)}
${esc(resource.title)}
${resource.description?`
${esc(resource.description)}
`:""}${resource.content_text?`
${esc(resource.content_text)}
`:""}${resource.file_path?`
File
${esc(resource.file_name||"Study material")}
`:""}${resource.external_url?`
External link
${esc(resource.external_url)}
`:""}${resource.is_published===false?`
This material is currently saved as a draft.
`:""} `;if(canManageResource(resource)){ body+=`
`; }modal("Study material",body); };window.UAdownloadResource=function(path,name,button){ resourceStorageDownload( decodeURIComponent(path), decodeURIComponent(name), button ); };window.UAopenExternalResource=function(encodedUrl){ const url=decodeURIComponent(encodedUrl); window.open(url,"_blank","noopener,noreferrer"); };window.UAdeleteResource=async function(resourceId,button){ const resource=learningResources.find(r=>r.id===resourceId); if(!resource||!canManageResource(resource))return;if(!confirm("Delete this study material?")){ return; }const restore=setButtonBusy(button,"Deleting…");try{ if(resource.file_path){ const s=await sessionReady();await fetch( `${CONFIG.SUPABASE_URL}/storage/v1/object/learning-resources/${resource.file_path.split("/").map(encodeURIComponent).join("/")}`, { method:"DELETE", headers:{ "apikey":CONFIG.SUPABASE_PUBLISHABLE_KEY, "Authorization":`Bearer ${s.access_token}` } } ); }await rest(`learning_resources?id=eq.${resourceId}`,{ method:"DELETE", prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("materials"); toast("Study material deleted."); }catch(e){ toast(e.message); }finally{ restore(); } };function renderAttendance(main){ if(isLearner()){ return renderStudentAttendance(main); }const today=iso(new Date()); const visible=[...classes].sort((a,b)=>{ const da=`${a.class_date} ${a.start_time||""}`; const db=`${b.class_date} ${b.start_time||""}`; return db.localeCompare(da); });main.innerHTML=`
Attendance
Mark attendance from scheduled calendar classes.
${visible.length?`
${visible.map(c=>{ const students=classStudents(c.id); const rows=attendanceForClass(c.id); const marked=new Set(rows.map(r=>r.student_id)); const done=students.length>0 && students.every(s=>marked.has(s.id));return `
${esc(c.title)}
${pretty(c.class_date)} · ${ft(c.start_time)}–${ft(c.end_time)}
${c.group_id?`
${esc(groupName(c.group_id))}
`:""}
today?"pending":"late"}"> ${done?"Marked":c.class_date>today?"Upcoming":`${marked.size}/${students.length} marked`}
`; }).join("")}
` :`
No calendar classes are available.
` } `; }function renderStudentAttendance(main){ const stats=attendanceStats(me.id);const rows=[...stats.rows].sort((a,b)=>{ const ca=classes.find(c=>c.id===a.class_id); const cb=classes.find(c=>c.id===b.class_id); return String(cb?.class_date||"").localeCompare(String(ca?.class_date||"")); });main.innerHTML=`
My attendance
Excused classes are not counted against attendance percentage.
${stats.percent===null?"—":`${stats.percent}%`}Attendance
${stats.present}Present
${stats.late}Late
${stats.absent}Absent
Attendance history
${rows.length?rows.map(r=>{ const c=classes.find(x=>x.id===r.class_id); return `
${esc(c?.title||"Class")}
${c?pretty(c.class_date):""} ${c?.start_time?` · ${ft(c.start_time)}`:""}
${esc(r.status.charAt(0).toUpperCase()+r.status.slice(1))}
${r.note?`
${esc(r.note)}
`:""}
`; }).join(""):`
Attendance has not been marked yet.
`}
`; }window.UAopenAttendance=function(classId){ const c=classes.find(x=>x.id===classId); if(!c)return;if(isLearner()){ return switchPortalTab("attendance"); }const students=classStudents(classId); if(!students.length){ return toast("No Student account is assigned to this class."); }const body=`
${esc(c.title)}
${pretty(c.class_date)} · ${ft(c.start_time)}–${ft(c.end_time)} ${c.group_id?`
${esc(groupName(c.group_id))}`:""}
${students.map(student=>{ const existing=attendanceRecord(classId,student.id); return `
${esc(student.full_name||student.email)} ${student.login_id?`
${esc(student.login_id)}
`:""}
`; }).join("")}
`;modal("Mark attendance",body);document.getElementById("uaMarkAllPresent").onclick=()=>{ document.querySelectorAll("#ua-scheduler .uaAttStatus") .forEach(select=>select.value="present"); };document.getElementById("uaSaveAttendance").onclick=function(){ saveAttendance(classId,this); }; };async function saveAttendance(classId,button){ const rows=[...document.querySelectorAll("#ua-scheduler .ua-att-row[data-student]")];const records=rows.map(row=>({ student_id:row.dataset.student, status:row.querySelector(".uaAttStatus").value, note:row.querySelector(".uaAttNote").value.trim()||null }));if(!records.length)return toast("No students are available.");const restore=setButtonBusy(button,"Saving…");try{ const count=await rest("rpc/save_class_attendance",{ method:"POST", body:{ p_class_id:classId, p_records:records } });restore(); closeModal(); await refresh(); renderMain("attendance");const saved=Number(count)||records.length; toast(`Attendance saved for ${saved} student${saved===1?"":"s"}.`); }catch(e){ toast(e.message); }finally{ restore(); } }function groupMembers(groupId,role=null){ return academicGroupMembers.filter( gm=>gm.group_id===groupId && (!role||gm.membership_role===role) ); }function renderGroups(main){ const canCreate=me.role==="admin";main.innerHTML=`
${canCreate?"Classes / Groups":"My classes"}
Continuing class relationships for multiple teachers and students.
${canCreate?``:""}
${academicGroups.length?`
${academicGroups.map(group=>{ const teachers=groupMembers(group.id,"teacher"); const students=groupMembers(group.id,"student"); return `
${esc(group.name)}
${esc(group.class_level||"")} ${group.class_level&&group.subject?" · ":""} ${esc(group.subject||"")}
${teachers.length} teacher${teachers.length===1?"":"s"} ${students.length} student${students.length===1?"":"s"} ${group.is_active===false?`Inactive`:""}
`; }).join("")}
` :`
${canCreate ?"No academic groups yet. Create the first group." :"No academic group is assigned to this account yet."}
` }`;if(canCreate){ main.querySelector("#uaAddGroup").onclick=openAddGroup; } }function memberOptions(role,selectedIds=[]){ return profiles .filter(p=>{ if(p.is_active===false)return false; if(role==="teacher")return p.role==="teacher"; return isStudentProfile(p); }) .map(p=>` `).join(""); }function openAddGroup(){ modal("Add class / group",`
You can select multiple teachers.
You can select multiple students.
`);document.getElementById("uaCreateGroup").onclick=function(){ createGroup(this); }; }async function createGroup(button){ const name=document.getElementById("uaGName").value.trim(); const level=document.getElementById("uaGLevel").value.trim(); const subject=document.getElementById("uaGSubject").value.trim();const teachers=[ ...document.getElementById("uaGTeachers").selectedOptions ].map(o=>o.value);const students=[ ...document.getElementById("uaGStudents").selectedOptions ].map(o=>o.value);if(!name)return toast("Enter a group name."); if(!teachers.length)return toast("Select at least one teacher."); if(!students.length)return toast("Select at least one student.");const restore=setButtonBusy(button,"Creating…"); let group=null;try{ const rows=await rest("academic_groups",{ method:"POST", body:{ name, class_level:level||null, subject:subject||null, created_by:me.id }, prefer:"return=representation" });group=rows?.[0]; if(!group)throw new Error("Group could not be created.");const members=[ ...teachers.map(user_id=>({ group_id:group.id, user_id, membership_role:"teacher" })), ...students.map(user_id=>({ group_id:group.id, user_id, membership_role:"student" })) ];await rest("academic_group_members",{ method:"POST", body:members, prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("groups"); toast("Group created."); }catch(e){ if(group?.id){ try{ await rest(`academic_groups?id=eq.${group.id}`,{ method:"DELETE", prefer:"return=minimal" }); }catch(_){} } toast(e.message); }finally{ restore(); } }window.UAopenGroup=function(groupId){ const group=academicGroups.find(g=>g.id===groupId); if(!group)return;const teacherRows=groupMembers(groupId,"teacher"); const studentRows=groupMembers(groupId,"student");const teachers=teacherRows .map(gm=>profiles.find(p=>p.id===gm.user_id)) .filter(Boolean);const students=studentRows .map(gm=>profiles.find(p=>p.id===gm.user_id)) .filter(Boolean);let body=`
Group${esc(group.name)}
Status${group.is_active===false?"Inactive":"Active"}
Class level${esc(group.class_level||"—")}
Subject${esc(group.subject||"—")}
Teachers ${teachers.length ?teachers.map(p=>`
${esc(p.full_name||p.email)} ${p.login_id?`
${esc(p.login_id)}
`:""}
`).join("") :`
No teacher assigned.
` }
Students / Parent-Student accounts ${students.length ?students.map(p=>`
${esc(p.full_name||p.email)} ${p.login_id?`
${esc(p.login_id)}
`:""}
`).join("") :`
No student assigned.
` }
`;if(me.role==="admin"){ body+=`
`; }modal("Class / group",body); };window.UAmanageGroup=function(groupId){ const group=academicGroups.find(g=>g.id===groupId); if(!group||me.role!=="admin")return;const selectedTeachers=groupMembers(groupId,"teacher").map(x=>x.user_id); const selectedStudents=groupMembers(groupId,"student").map(x=>x.user_id);closeModal();modal("Manage group",`
`);document.getElementById("uaSaveGroup").onclick=function(){ saveGroup(groupId,this); }; };async function saveGroup(groupId,button){ const name=document.getElementById("uaGEditName").value.trim(); const level=document.getElementById("uaGEditLevel").value.trim(); const subject=document.getElementById("uaGEditSubject").value.trim(); const isActive=document.getElementById("uaGEditActive").checked;const teachers=[ ...document.getElementById("uaGEditTeachers").selectedOptions ].map(o=>o.value);const students=[ ...document.getElementById("uaGEditStudents").selectedOptions ].map(o=>o.value);if(!name)return toast("Enter a group name."); if(!teachers.length)return toast("Select at least one teacher."); if(!students.length)return toast("Select at least one student.");const restore=setButtonBusy(button,"Saving…");try{ await rest(`academic_groups?id=eq.${groupId}`,{ method:"PATCH", body:{ name, class_level:level||null, subject:subject||null, is_active:isActive }, prefer:"return=minimal" });await rest(`academic_group_members?group_id=eq.${groupId}`,{ method:"DELETE", prefer:"return=minimal" });const members=[ ...teachers.map(user_id=>({ group_id:groupId, user_id, membership_role:"teacher" })), ...students.map(user_id=>({ group_id:groupId, user_id, membership_role:"student" })) ];await rest("academic_group_members",{ method:"POST", body:members, prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("groups"); toast("Group updated."); }catch(e){ toast(e.message); }finally{ restore(); } }function pendingForClass(classId){return proposals.find(p=>p.class_id===classId&&p.status==="pending")} function myPendingApprovals(){return approvals.filter(a=>a.approver_user_id===me.id&&a.decision==="pending")} function renderMain(tab="dashboard"){ const main=root.querySelector("#uaMain");if(!main)return;if( isParent() && !["dashboard","announcements","fees","family","saved","notifications"].includes(tab) ){ tab="dashboard"; activeTab="dashboard"; }if(tab==="dashboard")return renderDashboard(main); if(tab==="announcements")return renderAnnouncements(main); if(tab==="fees")return renderFees(main); if(tab==="groups")return renderGroups(main); if(tab==="homework")return renderHomework(main); if(tab==="attendance")return renderAttendance(main); if(tab==="materials")return renderStudyMaterial(main); if(tab==="courses")return renderCourses(main); if(tab==="tests")return renderAssessments(main); if(tab==="saved")return renderSavedArticles(main); if(tab==="family")return renderFamily(main); if(tab==="notifications")return renderNotifications(main); if(tab==="history")return renderHistory(main); const pending=proposals.filter(p=>p.status==="pending").length,mine=myPendingApprovals().length; main.innerHTML=`
${classes.length}Classes visible to you
${pending}Pending changes
${mine}Waiting for your response
${mine?`
${mine} schedule change${mine===1?" is":"s are"} waiting for your approval. Open a yellow class.
`:""}
${me.role==="admin"?``:""}
Swipe left/right to view the full monthly calendar.
Sun
Mon
Tue
Wed
Thu
Fri
Sat
`; main.querySelector("#uaPrev").onclick=()=>{viewDate=new Date(viewDate.getFullYear(),viewDate.getMonth()-1,1);drawCalendar()}; main.querySelector("#uaNext").onclick=()=>{viewDate=new Date(viewDate.getFullYear(),viewDate.getMonth()+1,1);drawCalendar()}; main.querySelector("#uaToday").onclick=()=>{viewDate=new Date();drawCalendar()}; if(me.role==="admin")main.querySelector("#uaAdd").onclick=openAddClass; drawCalendar(); } function drawCalendar(){ const grid=root.querySelector("#uaGrid"),month=root.querySelector("#uaMonth");if(!grid||!month)return; month.textContent=viewDate.toLocaleString("en-IN",{month:"long",year:"numeric"}); const y=viewDate.getFullYear(),m=viewDate.getMonth(),first=new Date(y,m,1),start=new Date(y,m,1-first.getDay()); grid.innerHTML=""; for(let i=0;i<42;i++){ const d=new Date(start);d.setDate(start.getDate()+i);const dayIso=iso(d); const cell=document.createElement("div");cell.className="ua-day"+(d.getMonth()!==m?" ua-out":"");cell.innerHTML=`
${d.getDate()}
`; classes.filter(c=>c.class_date===dayIso).forEach(c=>{ const p=pendingForClass(c.id),ev=document.createElement("button"); ev.className="ua-event "+(p?"yellow":"green"); ev.innerHTML=`
${ft(c.start_time)}
${esc(c.title)}
${p?"Change proposed":"Confirmed"}
`; ev.onclick=()=>openClass(c.id);cell.appendChild(ev); }); proposals.filter(p=>p.status==="pending"&&p.proposed_date===dayIso&&classes.some(c=>c.id===p.class_id&&c.class_date!==p.proposed_date)).forEach(p=>{ const c=classes.find(x=>x.id===p.class_id);if(!c)return; const ev=document.createElement("button");ev.className="ua-event yellow"; ev.innerHTML=`
${ft(p.proposed_start)}
${esc(c.title)}
Proposed time
`; ev.onclick=()=>openClass(c.id);cell.appendChild(ev); }); grid.appendChild(cell); } } function openClass(id){ const c=classes.find(x=>x.id===id);if(!c)return; const p=pendingForClass(id); let body=`
Class${esc(c.title)}
Current date${pretty(c.class_date)}
Current time${ft(c.start_time)}–${ft(c.end_time)}
Subject${esc(c.subject||"—")}
${c.group_id?`
Academic group${esc(groupName(c.group_id))}
`:""}
`; if(p){ const aa=approvals.filter(a=>a.proposal_id===p.id),my=aa.find(a=>a.approver_user_id===me.id); body+=`
🟡 Proposed change
New date${pretty(p.proposed_date)}
New time${ft(p.proposed_start)}–${ft(p.proposed_end)}
Reason${esc(p.reason||"Not provided")}
${aa.map(a=>`
${a.decision==="accepted"?"✅":a.decision==="rejected"?"❌":"⏳"} ${esc(a.approver_name||ROLE[a.approver_role])}
${ROLE[a.approver_role]} · ${esc(a.decision)}
`).join("")}
`; if(my&&my.decision==="pending"){ body+=`
`; } }else{ body+=`
`; }if(!isLearner()){ body+=`
`; }modal("Class schedule",body);const attBtn=document.getElementById("uaAttendanceShortcut"); if(attBtn){ attBtn.onclick=()=>{ closeModal(); window.UAopenAttendance(id); }; } } window.UAproposal=function(id){ const c=classes.find(x=>x.id===id);if(!c)return; modal("Propose schedule change",`
Current: ${pretty(c.class_date)}, ${ft(c.start_time)}–${ft(c.end_time)}
`); document.getElementById("uaSendProposal").onclick=function(){submitProposal(id,this)}; }; async function submitProposal(classId,button){ const date=document.getElementById("uaPDate").value,start=document.getElementById("uaPStart").value,end=document.getElementById("uaPEnd").value,reason=document.getElementById("uaReason").value.trim(); if(!date||!start||!end)return toast("Choose date and time."); if(!reason)return toast("Please enter a reason for the schedule change.");const restore=setButtonBusy(button,"Saving…"); let proposalId=null;try{ proposalId=await rest("rpc/create_schedule_proposal",{ method:"POST", body:{p_class_id:classId,p_date:date,p_start:start,p_end:end,p_reason:reason} });// The actual schedule proposal is saved. Stop the button loader now. restore(); closeModal(); await refresh(); renderMain("calendar"); toast("Schedule change proposed.");// Email delivery can take longer. Do not keep the UI/button spinning for it. await invokeFunction(proposalId,"proposal_created"); }catch(e){ toast(e.message); }finally{ restore(); } } window.UArespond=async function(proposalId,decision,button){ const restore=setButtonBusy(button,decision==="accepted"?"Accepting…":"Rejecting…");try{ const status=await rest("rpc/respond_to_schedule_proposal",{ method:"POST", body:{p_proposal_id:proposalId,p_decision:decision} });// Approval/rejection is already saved. Release the UI immediately. restore(); closeModal(); await refresh(); renderMain("calendar");toast( status==="accepted" ?"All approvals complete. Schedule updated." :status==="rejected" ?"Proposal rejected. Original schedule kept." :"Response saved." );// Email notification continues separately and no longer holds the loader. await invokeFunction(proposalId,"approval_response"); }catch(e){ toast(e.message); }finally{ restore(); } }; async function openAddClass(){ const groups=academicGroups.filter(g=>g.is_active!==false); if(!groups.length){ return toast("Create an academic group first from Classes / Groups."); }modal("Add class",`
`);const groupSelect=document.getElementById("uaCGroup"); const titleInput=document.getElementById("uaCTitle"); const subjectInput=document.getElementById("uaCSubject"); const teacherSelect=document.getElementById("uaCTeacher"); const summary=document.getElementById("uaCGroupSummary");function syncSelectedGroup(){ const group=academicGroups.find(g=>g.id===groupSelect.value); if(!group)return;const teachers=groupProfiles(group.id,"teacher"); const students=groupProfiles(group.id,"student");teacherSelect.innerHTML=teachers.map(p=>` `).join("");if(!titleInput.dataset.touched){ titleInput.value=group.name||""; } if(!subjectInput.dataset.touched){ subjectInput.value=group.subject||""; }summary.innerHTML=` ${esc(group.name)}
${teachers.length} teacher${teachers.length===1?"":"s"} · ${students.length} student${students.length===1?"":"s"}
All current group members will be added to this calendar event. `; }titleInput.addEventListener("input",()=>titleInput.dataset.touched="1"); subjectInput.addEventListener("input",()=>subjectInput.dataset.touched="1"); groupSelect.addEventListener("change",()=>{ titleInput.dataset.touched=""; subjectInput.dataset.touched=""; syncSelectedGroup(); });syncSelectedGroup();document.getElementById("uaCreateClass").onclick=function(){ createClass(this); }; }async function createClass(button){ const groupId=document.getElementById("uaCGroup").value; const title=document.getElementById("uaCTitle").value.trim(); const subject=document.getElementById("uaCSubject").value.trim(); const date=document.getElementById("uaCDate").value; const start=document.getElementById("uaCStart").value; const end=document.getElementById("uaCEnd").value; const teacher=document.getElementById("uaCTeacher").value;const memberIds=[ ...new Set(groupMemberIds(groupId)) ];const teacherIds=groupMemberIds(groupId,"teacher"); const studentIds=groupMemberIds(groupId,"student");if(!groupId||!title||!date||!start||!end){ return toast("Complete the academic group, title, date and time."); } if(!teacher||!teacherIds.includes(teacher)){ return toast("Select a primary teacher from this academic group."); } if(!studentIds.length){ return toast("This academic group has no students."); }const restore=setButtonBusy(button,"Creating…");try{ const rows=await rest("classes",{ method:"POST", body:{ title, subject:subject||null, class_date:date, start_time:start, end_time:end, teacher_id:teacher, created_by:me.id, group_id:groupId }, prefer:"return=representation" });const c=rows?.[0]; if(!c)throw new Error("Class was not returned by Supabase.");await rest("class_members",{ method:"POST", body:memberIds.map(id=>({ class_id:c.id, user_id:id })), prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("calendar"); toast("Class created from academic group."); }catch(e){ toast(e.message); }finally{ restore(); } }function myHomeworkSubmission(homeworkId){ return homeworkSubmissions.find( s=>s.homework_id===homeworkId && s.student_id===me.id ); }function homeworkBadge(hw){ // Parent / Student: show this account's own completion state first. if(isLearner()){ const sub=myHomeworkSubmission(hw.id); if(sub?.status==="checked")return {text:"Checked",cls:"checked"}; if(sub?.status==="submitted")return {text:"Submitted",cls:"completed"}; if(sub?.status==="completed")return {text:"Completed",cls:"completed"}; }// Admin / Teacher: calculate overall status from all Parent / Student // accounts assigned to this class. Once everyone has responded, the // due-date badge must no longer show Due soon / Overdue. if(me.role==="admin"||me.role==="teacher"){ const expected=expectedHomeworkStudents(hw); const expectedIds=expected.map(s=>s.id); const subs=homeworkSubmissions.filter( s=>s.homework_id===hw.id && expectedIds.includes(s.student_id) );if(expected.length>0){ const byStudent=new Map(subs.map(s=>[s.student_id,s])); const allResponded=expected.every(student=>{ const sub=byStudent.get(student.id); return sub && ["completed","submitted","checked"].includes(sub.status); });const allChecked=expected.every(student=>{ const sub=byStudent.get(student.id); return sub?.status==="checked"; });if(allChecked)return {text:"Checked",cls:"checked"}; if(allResponded)return {text:"Completed",cls:"completed"};const responded=expected.filter(student=>{ const sub=byStudent.get(student.id); return sub && ["completed","submitted","checked"].includes(sub.status); }).length;if(responded>0){ return {text:`${responded}/${expected.length} completed`,cls:"pending"}; } } }// Only pending homework should use due-date based badges. const now=Date.now(); const due=new Date(hw.due_at).getTime(); if(dueisStudentProfile(p)&&ids.includes(p.id) ); }const ids=classMembers .filter(cm=>cm.class_id===hw.class_id) .map(cm=>cm.user_id);return profiles.filter( p=>isStudentProfile(p)&&ids.includes(p.id) ); }function renderHomework(main){ const canCreate=me.role==="admin"||me.role==="teacher"; const items=[...homeworks].sort((a,b)=>new Date(a.due_at)-new Date(b.due_at));main.innerHTML=`
Homework
${isLearner()?"Assignments for your classes":"Create and review class homework"}
${canCreate?``:""}
${items.length?`
${items.map(hw=>{ const badge=homeworkBadge(hw); const expectedStudents=expectedHomeworkStudents(hw); const expectedIds=expectedStudents.map(s=>s.id); const subs=homeworkSubmissions.filter( s=>s.homework_id===hw.id && expectedIds.includes(s.student_id) && ["completed","submitted","checked"].includes(s.status) ); const expected=expectedStudents.length; return `
${esc(hw.title)}
${esc(homeworkTargetName(hw))}
${badge.text}
${esc(hw.instructions)}
`; }).join("")}
` :`
No homework has been assigned yet.
` }`;if(canCreate){ main.querySelector("#uaAddHomework").onclick=openAddHomework; } }async function openAddHomework(){ const available=academicGroups.filter(g=>g.is_active!==false);if(!available.length){ return toast( me.role==="admin" ?"Create an academic group first from Classes / Groups." :"No academic group is assigned to your account." ); }modal("Add homework",`
PDF, JPG, PNG or DOCX · maximum 10 MB
`);const groupSelect=document.getElementById("uaHWGroup"); const summary=document.getElementById("uaHWGroupSummary");function syncHomeworkGroup(){ const group=academicGroups.find(g=>g.id===groupSelect.value); if(!group)return;const teachers=groupMemberIds(group.id,"teacher").length; const students=groupMemberIds(group.id,"student").length;summary.innerHTML=` ${esc(group.name)}
${teachers} teacher${teachers===1?"":"s"} · ${students} student${students===1?"":"s"}
Homework will be visible to this academic group. `; }groupSelect.addEventListener("change",syncHomeworkGroup); syncHomeworkGroup();document.getElementById("uaCreateHomework").onclick=function(){ createHomework(this); }; }async function createHomework(button){ const groupId=document.getElementById("uaHWGroup").value; const title=document.getElementById("uaHWTitle").value.trim(); const instructions=document.getElementById("uaHWInstructions").value.trim(); const dueValue=document.getElementById("uaHWDue").value; const file=document.getElementById("uaHWFile").files?.[0]||null;if(!groupId||!title||!instructions||!dueValue){ return toast( "Academic group, title, instructions and due date are required." ); }if(!groupMemberIds(groupId,"student").length){ return toast("This academic group has no students."); }if(file&&!validHomeworkFile(file))return;const restore=setButtonBusy(button,"Assigning…"); let hw=null;try{ const rows=await rest("homework",{ method:"POST", body:{ group_id:groupId, class_id:null, title, instructions, due_at:new Date(dueValue).toISOString(), assigned_by:me.id }, prefer:"return=representation" });hw=rows?.[0]; if(!hw)throw new Error("Homework could not be created.");if(file){ const path= `homework/${hw.id}/${crypto.randomUUID()}-${safeFileName(file.name)}`;await storageUpload(path,file);await rest(`homework?id=eq.${hw.id}`,{ method:"PATCH", body:{ attachment_path:path, attachment_name:file.name, attachment_type:file.type||null, attachment_size:file.size }, prefer:"return=minimal" }); }restore(); closeModal(); await refresh(); activeTab="homework"; renderMain("homework"); toast("Homework assigned to academic group.");await invokeHomeworkFunction( hw.id, "homework_assigned", null ); }catch(e){ if(hw?.id){ try{ await rest(`homework?id=eq.${hw.id}`,{ method:"DELETE", prefer:"return=minimal" }); }catch(_){} } toast(e.message); }finally{ restore(); } }window.UAopenHomework=function(homeworkId){ const hw=homeworks.find(h=>h.id===homeworkId); if(!hw)return;const subs=homeworkSubmissions.filter(s=>s.homework_id===hw.id); const mySub=myHomeworkSubmission(hw.id);let body=`
Class / Group${esc(homeworkTargetName(hw))}
Due${formatDateTime(hw.due_at)}
Assigned by${esc(profileName(hw.assigned_by))}
Assigned${formatDateTime(hw.created_at)}
${esc(hw.title)}
${esc(hw.instructions)}
${hw.attachment_path?`
Attachment
${esc(hw.attachment_name||"Homework file")}
`:""} `;if(isLearner()){ body+=`
Your status
${mySub?esc(mySub.status):"Not completed yet"}
${mySub?.teacher_comment?`
Teacher comment:
${esc(mySub.teacher_comment)}
`:""}
PDF, JPG, PNG or DOCX · maximum 10 MB
${mySub?.submission_path?`
Your submitted file
${esc(mySub.submission_name||"Submission")}
`:""}
`; }else{ const expected=expectedHomeworkStudents(hw); body+=`
Student status (${subs.length}/${expected.length}) ${expected.length?expected.map(student=>{ const sub=subs.find(s=>s.student_id===student.id); return `
${esc(student.full_name||student.email)}
${sub?esc(sub.status):"Pending"}
${sub?.submission_path?` `:""}
${sub?.note?`
${esc(sub.note)}
`:""} ${sub&&sub.status!=="checked"?`
`:""}
`; }).join(""):`
No Student account is assigned to this class / group.
`}
`; }modal("Homework",body); };window.UAdownloadHomework=function(path,name,button){ storageDownload(decodeURIComponent(path),decodeURIComponent(name),button); };window.UAsubmitHomework=async function(homeworkId,mode,button){ const note=document.getElementById("uaHWSubNote")?.value.trim()||""; const file=document.getElementById("uaHWSubmissionFile")?.files?.[0]||null;if(mode==="submitted"&&!file&&!myHomeworkSubmission(homeworkId)?.submission_path){ return toast("Choose a file, or use Mark completed."); } if(file&&!validHomeworkFile(file))return;const restore=setButtonBusy(button,mode==="submitted"?"Submitting…":"Saving…");try{ const existing=myHomeworkSubmission(homeworkId); let path=existing?.submission_path||null; let name=existing?.submission_name||null; let type=existing?.submission_type||null; let size=existing?.submission_size||null;if(file){ path=`submissions/${homeworkId}/${me.id}/${crypto.randomUUID()}-${safeFileName(file.name)}`; await storageUpload(path,file); name=file.name; type=file.type||null; size=file.size; }const status=file||path?"submitted":"completed";const submissionRows=await rest( "homework_submissions?on_conflict=homework_id,student_id", { method:"POST", body:{ homework_id:homeworkId, student_id:me.id, status:mode==="completed"&&!file&&!path?"completed":status, note:note||null, submission_path:path, submission_name:name, submission_type:type, submission_size:size, submitted_at:new Date().toISOString(), checked_by:null, checked_at:null }, prefer:"resolution=merge-duplicates,return=representation" } );const savedSubmission=submissionRows?.[0]||null;restore(); closeModal(); await refresh(); renderMain("homework"); toast( mode==="completed" ?"Homework marked completed." :"Homework submitted." );await invokeHomeworkFunction( homeworkId, mode==="completed" ?"homework_completed" :"homework_submitted", savedSubmission?.id||null ); }catch(e){ toast(e.message); }finally{ restore(); } };window.UAcheckHomework=async function(submissionId,button){ const restore=setButtonBusy(button,"Checking…"); const before=homeworkSubmissions.find( s=>s.id===submissionId );try{ await rest(`homework_submissions?id=eq.${submissionId}`,{ method:"PATCH", body:{ status:"checked", checked_by:me.id, checked_at:new Date().toISOString() }, prefer:"return=minimal" });restore(); closeModal(); await refresh(); renderMain("homework"); toast("Homework marked checked.");if(before?.homework_id){ await invokeHomeworkFunction( before.homework_id, "homework_checked", submissionId ); } }catch(e){ toast(e.message); }finally{ restore(); } };function renderNotifications(main){ main.innerHTML=`
Notification delivery log

You see your own delivery records; Admin can see all.

${notifications.length?notifications.map(n=>`
${new Date(n.created_at).toLocaleString("en-IN")} · ${esc(n.channel)} · ${esc(n.status)}
${esc(n.message)}
`).join(""):`

No notifications yet.

`}
`; } function renderHistory(main){ const sorted=[...proposals].sort((a,b)=>new Date(b.created_at)-new Date(a.created_at)); main.innerHTML=`
Schedule change history${sorted.length?sorted.map(p=>{const c=classes.find(x=>x.id===p.class_id);return `
${new Date(p.created_at).toLocaleString("en-IN")} · ${esc(p.status)}
${esc(c?.title||"Class")}
Proposed: ${pretty(p.proposed_date)} at ${ft(p.proposed_start)}${p.reason?` · ${esc(p.reason)}`:""}
`}).join(""):`

No schedule changes yet.

`}
`; } function modal(title,body){ const x=document.createElement("div");x.id="uaModal";x.className="ua-backdrop"; x.innerHTML=`
${esc(title)}
${body}
`; root.appendChild(x); x.querySelector("#uaClose").onclick=closeModal; x.onclick=e=>{if(e.target===x)closeModal()}; } function closeModal(){document.getElementById("uaModal")?.remove()} init(); } catch (error) { console.error("unipolaris scheduler fatal error:", error); const root = document.getElementById("ua-scheduler"); if (root) { root.innerHTML = '
' + 'Scheduler startup error

' + String(error && error.message ? error.message : error) + '
'; } } }if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", startUnipolarisScheduler); } else { startUnipolarisScheduler(); } })();