Online Tuition for Class 7 CBSE

online tuition coaching for class 7 cbse

unipolaris academy provides good quality, interactive and personalised online tuition for Class 7 CBSE for Maths and Science subjects. At unipolaris academy, we offer comprehensive CBSE online tuition for Class 7, designed to help students grasp key concepts with clarity and confidence.

Benefits in Online Classes for Class 7 CBSE

Qualified and Experienced Online Classes Tutors for Class 7 CBSE with pen tablet

One to One teacher to student ratio

Regular Assignments and their evaluation

Questions complementing school curriculum AND MCQs(Multiple choice questions), VSA (Very Short Answers)(SA-I), SA(Short Answers)(SA-II) & LA(Long Answer), Case based/Passage based questions are given for practice and solved along with students

Core books followed are NCERT and NCERT exemplar

Regular Doubts Resolution

Recording after the live class is provided in a folder for revision

Focus is on clearing the basics of the student

Personal Attention to the Student for focused learning

Free Live Demo offered before Parent and student decides to go for regular classes

Subjects Offered in Online Coaching for Class 7 CBSE

Maths

Concepts Covered

  • Large Numbers around us
  • Arithmetic expressions
  • A peek beyond the point
  • Expressions using letter-numbers
  • Parallel and Intersecting Lines
  • Number Play
  • A Tale of Three Intersecting Lines
  • Working with fractions

Science

Concepts Covered

  • The Ever-Evolving World of Science
  • Exploring Substances: Acidic, Basic, and Neutral
  • Electricity: Circuits and their Components
  • The World of Metals and Non-metals
  • Changes Around Us: Physical and Chemical
  • Adolescence: A Stage of Growth and Change
  • Heat Transfer in Nature
  • Measurement of Time and Motion
  • Life Processes in Animals
  • Life Processes in Plants
  • Light: Shadows and Reflections
  • Earth, Moon, and the Sun

Testimonials

“My son was struggling with Class 7 Maths and Science, but these online classes have made a huge difference. The teachers explain concepts clearly and provide plenty of practice problems. His confidence has improved, and so have his grades!”
Priya Malhotra, Parent

“I used to find Maths really difficult, but after joining these online tuition classes, everything makes sense! The teachers are so patient and explain things step by step. Now, I actually enjoy solving problems!”
Arjun Verma, Class 7 Student

“The best part about these online classes is the flexibility and personal attention. My daughter can learn at her own pace, and the doubt-clearing sessions have been really helpful. Highly recommended for Class 7 CBSE students!”
Rajeev Khanna, Parent

“Science used to be confusing for me, but the teachers here make it fun and easy to understand. The real-life examples and experiments they explain help me remember concepts better. My test scores have improved a lot!”
Meera Joshi, Class 7 Student

“These online classes provide a structured learning experience with expert teachers and great study materials. The weekly quizzes and assignments keep my child engaged and motivated. It’s the perfect solution for CBSE students!”
Suman Agarwal, Parent

Frequently Asked Questions (FAQs)

How is the curriculum structured?

Our curriculum is structured to align with the CBSE syllabus for Class 7. We cover Mathematics and Science subjects. Each subject is broken down into modules and chapters, with a focus on understanding fundamental concepts, practicing problems, and applying knowledge through interactive activities and assessments.

How do you ensure the quality of teaching?

Quality is our top priority. All our tutors are highly qualified, experienced, and undergo a rigorous selection process. They receive ongoing training to stay updated with the latest teaching methodologies and CBSE guidelines. We also gather regular feedback from students and parents to continuously improve our teaching methods.

How do you personalise the learning experience?

We understand that each student has unique learning needs. Our one to one teacher to student ratio allows for personalized attention. Tutors identify the individual strengths and weaknesses of each student and tailor their teaching methods accordingly.

Do teachers check the assignments they provide?

Yes, our teachers provide assignments and evaluate them regularly

What is different about unipolaris academy?

Our well qualified and experienced teachers, sincerity, punctuality, personal attention to the child, good intention to provide value to the student are key differentiators.

What is the fees structure?

You can check the fees structure here: FEES

How much time will it take to finish the course?

It depends on mutual understanding and pace between teacher and student but roughly it will take 6 to 9 months to finish the syllabus if we go at 2 or 3 classes per week

`;popup.document.open(); popup.document.write(html); popup.document.close(); };function formatFeeMoney(minor,currency="INR"){ const amount=Number(minor||0)/100;try{ return new Intl.NumberFormat("en-IN",{ style:"currency", currency:currency||"INR", minimumFractionDigits:0, maximumFractionDigits:2 }).format(amount); }catch(_){ return `${currency||"INR"} ${amount.toFixed(2)}`; } }function feePaymentsForCharge(chargeId){ return feePayments .filter(payment=>payment.charge_id===chargeId) .sort( (a,b)=> String(b.paid_at||"") .localeCompare(String(a.paid_at||"")) ); }function feeDisplayStatus(charge){ if(!charge)return "pending";if( charge.status==="pending" || charge.status==="partial" ){ const due=new Date(`${charge.due_date}T23:59:59`); if(due.getTime() ["pending","partial"].includes(charge.status) ?sum+Number(charge.balance_amount_minor||0) :sum, 0 ); }function feeOverdueCount(){ return feeCharges.filter( charge=>feeDisplayStatus(charge)==="overdue" ).length; }function feeReceivedThisMonthMinor(){ const now=new Date(); const y=now.getFullYear(); const m=now.getMonth();return feePayments .filter(payment=>{ if(payment.status!=="received")return false;const d=new Date(payment.paid_at); return ( d.getFullYear()===y && d.getMonth()===m ); }) .reduce( (sum,payment)=> sum+Number(payment.amount_minor||0), 0 ); }function billingMonthLabel(value){ if(!value)return "";const d=new Date(`${value.slice(0,7)}-01T00:00:00`);return d.toLocaleDateString( "en-IN", { month:"long", year:"numeric" } ); }function feeGroupLabel(groupId){ if(!groupId)return "General tuition";const group=academicGroups.find( g=>g.id===groupId );return group?.name||"Academic Group"; }function renderFees(main){ const isAdmin=me.role==="admin";main.innerHTML=`
Fees & Receipts
Regular academy tuition fees and payment receipts.
${isAdmin?` `:""}
${formatFeeMoney(feeOutstandingMinor())} Outstanding balance
${feeOverdueCount()} Overdue fee record${feeOverdueCount()===1?"":"s"}
${formatFeeMoney(feeReceivedThisMonthMinor())} ${isAdmin?"Received this month":"Paid this month"}
`;const createBtn=main.querySelector("#uaCreateFeeCharge"); if(createBtn){ createBtn.onclick=openCreateFeeCharge; }const filter=main.querySelector("#uaFeeFilter"); filter.onchange=()=>{ renderFeeCards( main.querySelector("#uaFeeCards"), filter.value ); };renderFeeCards( main.querySelector("#uaFeeCards"), "all" ); }function renderFeeCards(container,filter){ let rows=[...feeCharges];if(filter==="pending"){ rows=rows.filter( charge=> charge.status==="pending" || charge.status==="partial" ); }else if(filter==="overdue"){ rows=rows.filter( charge=>feeDisplayStatus(charge)==="overdue" ); }else if(filter!=="all"){ rows=rows.filter( charge=>charge.status===filter ); }rows.sort((a,b)=>{ const statusRank={ overdue:4, partial:3, pending:2, paid:1, waived:0, cancelled:-1 };const sa=statusRank[feeDisplayStatus(a)]??0; const sb=statusRank[feeDisplayStatus(b)]??0;if(sb!==sa)return sb-sa;return String(b.billing_month) .localeCompare(String(a.billing_month)); });container.innerHTML=rows.length?`
${rows.map(charge=>{ const student=profiles.find( p=>p.id===charge.student_id );const displayStatus=feeDisplayStatus(charge);return `
${esc(student?.full_name||student?.email||"Student")}
${esc(billingMonthLabel(charge.billing_month))} · ${esc(feeGroupLabel(charge.group_id))}
${esc(feeStatusLabel(charge))}
${formatFeeMoney(charge.balance_amount_minor,charge.currency)}
${displayStatus==="paid"?"No balance due":"Balance due"}
Fee
${formatFeeMoney(charge.gross_amount_minor,charge.currency)}
Discount
${formatFeeMoney(charge.discount_minor,charge.currency)}
Paid
${formatFeeMoney(charge.paid_amount_minor,charge.currency)}
Due: ${esc(charge.due_date)}
`; }).join("")}
`:`
No fee records in this view.
`; }function activeStudentProfiles(){ return profiles.filter( p=> isStudentProfile(p) && p.is_active!==false ); }function groupsForStudent(studentId){ const ids=new Set( academicGroupMembers .filter( row=> row.user_id===studentId && row.membership_role==="student" ) .map(row=>row.group_id) );return academicGroups.filter( group=> ids.has(group.id) && group.is_active!==false ); }function monthInputValue(date=new Date()){ const y=date.getFullYear(); const m=String(date.getMonth()+1).padStart(2,"0"); return `${y}-${m}`; }function dateInputValue(date=new Date()){ const y=date.getFullYear(); const m=String(date.getMonth()+1).padStart(2,"0"); const d=String(date.getDate()).padStart(2,"0"); return `${y}-${m}-${d}`; }function openCreateFeeCharge(){ const students=activeStudentProfiles();if(!students.length){ return toast("No Student account is available."); }modal("Add Monthly Tuition Fee",`
`);const studentSelect=document.getElementById("uaFeeStudent"); const groupSelect=document.getElementById("uaFeeGroup");function syncGroups(){ const groups=groupsForStudent(studentSelect.value);groupSelect.innerHTML= ``+ groups.map(group=>` `).join(""); }studentSelect.addEventListener("change",syncGroups); syncGroups();document.getElementById("uaSaveFeeCharge").onclick=function(){ createFeeCharge(this); }; }async function createFeeCharge(button){ const studentId= document.getElementById("uaFeeStudent").value;const groupId= document.getElementById("uaFeeGroup").value||null;const month= document.getElementById("uaFeeMonth").value;const due= document.getElementById("uaFeeDue").value;const description= document.getElementById("uaFeeDescription").value.trim();const gross= Number(document.getElementById("uaFeeGross").value||0);const discount= Number(document.getElementById("uaFeeDiscount").value||0);const note= document.getElementById("uaFeeNote").value.trim();if(!studentId||!month||!due){ return toast("Student, billing month and due date are required."); }if(!Number.isFinite(gross)||gross<0){ return toast("Enter a valid monthly fee."); }if( !Number.isFinite(discount) || discount<0 || discount>gross ){ return toast("Discount cannot exceed the monthly fee."); }const restore=setButtonBusy(button,"Creating…");try{ const result=await rest( "rpc/admin_create_tuition_fee_charge", { method:"POST", body:{ p_student_id:studentId, p_group_id:groupId, p_billing_month:`${month}-01`, p_due_date:due, p_gross_amount_minor:Math.round(gross*100), p_discount_minor:Math.round(discount*100), p_description:description||"Monthly tuition fee", p_note:note||null } } );restore(); closeModal(); await refresh();if(result?.charge_id){ window.UAopenFeeCharge(result.charge_id); }else{ renderMain("fees"); }toast("Fee record created."); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAopenFeeCharge=function(chargeId){ const charge=feeCharges.find( c=>c.id===chargeId );if(!charge)return;const student=profiles.find( p=>p.id===charge.student_id );const chargePayments= feePaymentsForCharge(charge.id);const displayStatus=feeDisplayStatus(charge);let body=`
Student ${esc(student?.full_name||student?.email||"Student")}
Month ${esc(billingMonthLabel(charge.billing_month))}
Class / Group ${esc(feeGroupLabel(charge.group_id))}
Status ${esc(feeStatusLabel(charge))}
Due date ${esc(charge.due_date)}
Description ${esc(charge.description)}
Monthly fee
${formatFeeMoney(charge.gross_amount_minor,charge.currency)}
Discount
${formatFeeMoney(charge.discount_minor,charge.currency)}
Net fee
${formatFeeMoney(charge.net_amount_minor,charge.currency)}
Paid
${formatFeeMoney(charge.paid_amount_minor,charge.currency)}
Balance
${formatFeeMoney(charge.balance_amount_minor,charge.currency)}
${charge.note?`
${esc(charge.note)}
`:""}
Payments & Receipts${chargePayments.length ?chargePayments.map(payment=>`
${esc(payment.receipt_number)}
${formatDateTime(payment.paid_at)} · ${esc(payment.payment_method.replace("_"," "))} ${payment.reference?` · ${esc(payment.reference)}`:""}
${payment.status==="reversed"?`
Reversed ${payment.reversal_reason?` · ${esc(payment.reversal_reason)}`:""}
`:""}
${formatFeeMoney(payment.amount_minor,payment.currency)}
${me.role==="admin"&&payment.status==="received"?` `:""}
`).join("") :`
No payment recorded yet.
` }
`;if( me.role==="admin" && ["pending","partial"].includes(charge.status) && Number(charge.balance_amount_minor)>0 ){ body+=`
${Number(charge.paid_amount_minor)===0?` `:""}
`; }modal("Fee Details",body); };window.UArecordFeePayment=function(chargeId){ const charge=feeCharges.find( c=>c.id===chargeId );if(!charge||me.role!=="admin")return;closeModal();modal("Record Tuition Fee Payment",`
Outstanding balance: ${formatFeeMoney(charge.balance_amount_minor,charge.currency)}
`);document.getElementById("uaSaveFeePayment").onclick=function(){ saveFeePayment(chargeId,this); }; };async function saveFeePayment(chargeId,button){ const charge=feeCharges.find( c=>c.id===chargeId );if(!charge)return;const amount= Number(document.getElementById("uaFeePayAmount").value||0);const method= document.getElementById("uaFeePayMethod").value;const date= document.getElementById("uaFeePayDate").value;const reference= document.getElementById("uaFeePayReference").value.trim();const note= document.getElementById("uaFeePayNote").value.trim();const amountMinor=Math.round(amount*100);if( !Number.isFinite(amount) || amount<=0 ){ return toast("Enter a valid payment amount."); }if(amountMinor>Number(charge.balance_amount_minor)){ return toast("Payment cannot exceed the outstanding balance."); }const paidAt=date ?new Date(`${date}T12:00:00`).toISOString() :new Date().toISOString();const restore=setButtonBusy(button,"Recording…");try{ const result=await rest( "rpc/admin_record_tuition_fee_payment", { method:"POST", body:{ p_charge_id:chargeId, p_amount_minor:amountMinor, p_payment_method:method, p_reference:reference||null, p_paid_at:paidAt, p_note:note||null } } );restore(); closeModal(); await refresh();window.UAopenFeeCharge(chargeId);toast( result?.receipt_number ?`Payment recorded. Receipt ${result.receipt_number}` :"Payment recorded." ); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAreverseFeePayment=function(paymentId){ const payment=feePayments.find( p=>p.id===paymentId );if(!payment||me.role!=="admin")return;closeModal();modal("Reverse Payment",`
Receipt: ${esc(payment.receipt_number)}
Amount: ${formatFeeMoney(payment.amount_minor,payment.currency)}
The receipt remains in history and will be marked Reversed.
`);document.getElementById("uaConfirmFeeReverse").onclick=function(){ reverseFeePayment(paymentId,this); }; };async function reverseFeePayment(paymentId,button){ const payment=feePayments.find( p=>p.id===paymentId );if(!payment)return;const reason= document.getElementById("uaFeeReverseReason").value.trim();const restore=setButtonBusy(button,"Reversing…");try{ await rest( "rpc/admin_reverse_tuition_fee_payment", { method:"POST", body:{ p_payment_id:paymentId, p_reason:reason||"Admin correction" } } );restore(); closeModal(); await refresh(); window.UAopenFeeCharge(payment.charge_id); toast("Payment reversed."); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAcancelFeeCharge=function(chargeId){ if(me.role!=="admin")return;closeModal();modal("Cancel Fee Record",`
Use this only for a fee record created by mistake.
`);document.getElementById("uaConfirmFeeCancel").onclick=function(){ cancelFeeCharge(chargeId,this); }; };async function cancelFeeCharge(chargeId,button){ const reason= document.getElementById("uaFeeCancelReason").value.trim();const restore=setButtonBusy(button,"Cancelling…");try{ await rest( "rpc/admin_cancel_tuition_fee_charge", { method:"POST", body:{ p_charge_id:chargeId, p_reason:reason||null } } );restore(); closeModal(); await refresh(); renderMain("fees"); toast("Fee record cancelled."); }catch(e){ toast(e.message); }finally{ restore(); } }window.UAprintFeeReceipt=function(paymentId){ const payment=feePayments.find( p=>p.id===paymentId );if(!payment)return;const charge=feeCharges.find( c=>c.id===payment.charge_id );if(!charge)return;const student=profiles.find( p=>p.id===charge.student_id );const receiptWindow=window.open( "", "_blank", "noopener,noreferrer" );if(!receiptWindow){ return toast("Allow pop-ups to open the receipt."); }const studentName= student?.full_name||student?.email||"Student";const paymentDate= new Date(payment.paid_at) .toLocaleDateString("en-IN",{ day:"2-digit", month:"long", year:"numeric" });const method= payment.payment_method .replace("_"," ") .replace(/\b\w/g,c=>c.toUpperCase());const receiptHtml=` ${esc(payment.receipt_number)}

unipolaris academy

Tuition fee receipt

${payment.status==="reversed"?`
REVERSED RECEIPT
`:""}
Receipt no. ${esc(payment.receipt_number)}
Date ${esc(paymentDate)}
Student ${esc(studentName)}
Billing month ${esc(billingMonthLabel(charge.billing_month))}
Fee for ${esc(charge.description)}
Payment method ${esc(method)}
${payment.reference?`
Reference ${esc(payment.reference)}
`:""}
Amount received ${esc(formatFeeMoney(payment.amount_minor,payment.currency))}
Total fee after discount ${esc(formatFeeMoney(charge.net_amount_minor,charge.currency))}
Balance after this payment* ${esc(formatFeeMoney(charge.balance_amount_minor,charge.currency))}
`;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","results","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==="results")return renderResultsReports(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(); } })();