Champions Core HUB

Checking your secure login…

Sign in
BUILD THE MINDSET. BECOME THE CHAMPION.

Dashboard

`;const w=window.open('','_blank');if(!w){alert('Allow pop-ups to open the printable lesson receipt.');return;}w.document.open();w.document.write(receipt);w.document.close();} function parentPaymentsV16(){const pay=db.payments.filter(p=>p.parentId===parentId&&!p.voided);const paid=pay.filter(p=>p.status==='Confirmed');return panel('My payments and receipts',`
${btn('Export payment history (CSV)',"exportRows('payments')",true)}${btn('Print','window.print()')}
`+table(['Lesson','Amount','Method','Status','Action'],pay.map(p=>{let l=db.lessons.find(x=>x.id===p.lessonId);return row([l?fmt(l.date,l.time):'—',money(p.amount),esc(p.method||'Not selected'),esc(p.status),p.status==='Confirmed'?btn('Lesson receipt',`receiptV16('${p.id}')`,true):p.status==='Awaiting payment'?btn('Continue to payment',`paymentModal('${p.id}')`,true):'—'])}))) } views['My Payments']=parentPaymentsV16; // The original preview uses this function for the parent payments menu. parentPayments=parentPaymentsV16; render(); // Version 17: admin financial controls, appointment calendars, cancellation workflow, focused printing. const paidStatusV17=s=>['paid','confirmed'].includes(String(s||'').toLowerCase()); function changeIncomeStatusV17(kind,rid,st){if(role!=='admin')return;const x=(kind==='payment'?db.payments:db.income).find(v=>String(v.id)===String(rid));if(!x)return;if(kind==='payment'){if(st==='Paid')x.status='Confirmed';else if(st==='Outstanding')x.status='Awaiting payment';else x.status='Awaiting verification';}else x.status=st;x.statusChangedAt=new Date().toISOString();save();render();} function incomeSearchUpdate(){const input=$('income-search'),out=$('income-filter-results');if(!input||!out)return;const q=input.value.trim().toLowerCase(),matches=incomeAllRecords().filter(x=>[x.description,x.facility,x.type,x.method,x.source,x.status].some(v=>String(v||'').toLowerCase().includes(q))),paid=matches.filter(x=>paidStatusV17(x.status));$('income-filter-total').textContent=money(paid.reduce((s,x)=>s+Number(x.amount||0),0));$('income-filter-count').textContent=paid.length+' paid transactions';$('income-filter-label').textContent=q?'Matching confirmed income':'Total confirmed income';out.innerHTML=table(['Date','Description','Facility','Amount','Status','Manage'],matches.map(x=>row([esc(x.date),esc(x.description),esc(x.facility||'—'),money(x.amount),``,btn('Remove',`voidIncomeV16('${x.recordKind}','${x.id}')`)])));const c=$('income-history-count');if(c)c.textContent=matches.length+' transactions';} const incomeBaseV17=finIncomeV12; finIncomeV12=function(){let html=incomeBaseV17(),records=incomeAllRecords(),outstanding=records.filter(x=>!paidStatusV17(x.status)).reduce((s,x)=>s+Number(x.amount||0),0);html=html.replace('
',`
`);return html;};views['Income']=()=>finIncomeV12(); function lessonCancelV17(lid){if(role!=='admin')return;const l=db.lessons.find(x=>x.id===lid);if(!l||l.status==='Cancelled')return;modal('Cancel lesson · Notify family',`

${esc(aname(l.athleteId))} · ${esc(fmt(l.date,l.time))} · ${esc(l.facility)}

This is a local preview. A message is recorded here, but no email or text is sent.

`+form(sel('Offer to family','resolution',['Offer replacement date/time','Offer facility-specific Champions Credits','Offer refund','Discuss options with parent'])+txt('Message to parent','message','I need to cancel our scheduled lesson. I apologize for the inconvenience. We can arrange another date, apply a lesson credit, or discuss a refund.')+field('Proposed replacement date (optional)','date','','date')+field('Proposed replacement time (optional)','time','','time'),`saveCancelV17`,'Cancel lesson and record notice',`data-id="${l.id}"`));} function saveCancelV17(f){const l=db.lessons.find(x=>x.id===f.dataset.id);if(!l)return;const resolution=val(f,'resolution'),message=val(f,'message');if(!confirm('Cancel this booking? No external message or refund will be sent automatically.'))return;l.status='Cancelled';l.cancelResolution=resolution;l.cancelMessage=message;l.cancelDate=val(f,'date');l.cancelTime=val(f,'time');l.cancelledAt=new Date().toISOString();if(!db.notifications)db.notifications=[];db.notifications.unshift({id:id(),date:new Date().toISOString(),message:`Coach cancelled ${aname(l.athleteId)} lesson on ${l.date}. Parent message (preview only): ${message}. Option: ${resolution}`});save();closeModal();render();} function cancelButtonV17(l){return l.status==='Available'||l.status==='Cancelled'?'—':btn('Cancel',`lessonCancelV17('${l.id}')`)} function calendarMonthV17(isParent=false){const key=isParent?'parent':'admin';if(!window.calendarMonthsV17)window.calendarMonthsV17={};let cursor=window.calendarMonthsV17[key]||today().slice(0,7),[y,m]=cursor.split('-').map(Number),first=new Date(y,m-1,1),days=new Date(y,m,0).getDate(),offset=first.getDay();let cells=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].map(x=>`
${x}
`).join('');for(let i=0;i
';for(let d=1;d<=days;d++){let date=`${y}-${String(m).padStart(2,'0')}-${String(d).padStart(2,'0')}`,items=db.lessons.filter(l=>l.date===date&&(isParent?l.status==='Available':true)),available=items.filter(l=>l.status==='Available').length,booked=items.filter(l=>l.status!=='Available'&&l.status!=='Cancelled').length;cells+=``}return panel(isParent?'Find a lesson · Month calendar':'Full appointment calendar',`
${btn('◀ Previous',`shiftCalendarV17('${key}',-1)`)}${first.toLocaleString('en-US',{month:'long',year:'numeric'})}${btn('Next ▶',`shiftCalendarV17('${key}',1)`)}

Select any date to see times and ${isParent?'book or request an appointment':'manage availability and booked lessons'}.

${cells}
`)} function shiftCalendarV17(key,step){let [y,m]=(window.calendarMonthsV17?.[key]||today().slice(0,7)).split('-').map(Number);let d=new Date(y,m-1+step,1);window.calendarMonthsV17[key]=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;render();} function calendarDayV17(date,isParent){const ls=db.lessons.filter(l=>l.date===date&&(isParent?l.status==='Available':true)).sort((a,b)=>a.time.localeCompare(b.time));const actions=ls.map(l=>`
${esc(fmt(l.date,l.time))} · ${esc(l.facility)} · ${esc(l.status)} · ${money(l.price)} ${isParent?btn('Book',`bookSlot('${l.id}')`,true):l.status==='Available'?btn('Remove opening',`removeSlotV17('${l.id}')`):cancelButtonV17(l)}
`).join('');modal('Appointments · '+date,`

${ls.length?ls.length+' appointment(s)':'No appointments posted for this date.'}

${actions}
${isParent?btn('Request another time',`closeModal();requestLessonV17('${date}')`,true):btn('Post availability',`closeModal();go('Calendar & Availability');setTimeout(()=>{let el=document.querySelector('input[name=date]');if(el)el.value='${date}'},30)`,true)}
`);} function removeSlotV17(lid){const l=db.lessons.find(x=>x.id===lid);if(!l||l.status!=='Available'||!confirm('Remove this available time?'))return;db.lessons=db.lessons.filter(x=>x.id!==lid);save();closeModal();render();} function requestLessonV17(date){requestLesson();setTimeout(()=>{const el=document.querySelector('#modal input[name=date], .modal input[name=date]');if(el)el.value=date},30)} function adminCalendarV17(){const available=db.lessons.filter(l=>l.status==='Available'),booked=db.lessons.filter(l=>l.status!=='Available'&&l.status!=='Cancelled');return calendarMonthV17(false)+panel('Post available lessons',form(field('Date','date',today(),'date')+field('Time','time','','time')+sel('Facility','facility',['World Cup East Brunswick','Legacy Cheer'])+field('Duration (minutes)','duration','30','number')+field('Price ($)','price','55','number','min="0"'),'postSlot','Post availability'))+panel(`Booked appointments (${booked.length})`,table(['Date & time','Athlete','Facility','Status','Price','Action'],booked.map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.status),money(l.price),cancelButtonV17(l)]))))+panel(`Available appointments (${available.length})`,table(['Date & time','Facility','Price','Action'],available.map(l=>row([fmt(l.date,l.time),esc(l.facility),money(l.price),btn('Remove opening',`removeSlotV17('${l.id}')`)]))))} views['Calendar & Availability']=adminCalendarV17; const bookBaseV17=views['Book a Lesson'];views['Book a Lesson']=()=>calendarMonthV17(true)+bookBaseV17(); function printLessonHistoryV17(){document.body.classList.add('print-lesson-only');window.print();setTimeout(()=>document.body.classList.remove('print-lesson-only'),1000)} window.addEventListener('afterprint',()=>document.body.classList.remove('print-lesson-only')); function parentLessonsV17(){let lessons=db.lessons.filter(l=>l.parentId===parentId&&l.status!=='Available');return `
`+panel('My lessons — upcoming and past',`
${btn('Export lesson history (CSV)',"exportRows('lessons')",true)}${btn('Print lesson history','printLessonHistoryV17()')}
`+table(['Date & time','Athlete','Facility','Status','Action'],lessons.sort((a,b)=>(b.date+b.time).localeCompare(a.date+a.time)).map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.status),l.status==='Pending payment'?btn('Pay',`payForLesson('${l.id}')`,true):['Confirmed','Awaiting verification'].includes(l.status)?btn('Request reschedule',`reschedule('${l.id}')`):'—']))))+'
'} views['My Lessons']=parentLessonsV17;parentLessons=parentLessonsV17; function recentActivitiesV17(){const now=new Date(),weekAgo=new Date(now.getTime()-7*86400000);let all=(db.notifications||[]).slice().sort((a,b)=>String(b.date).localeCompare(String(a.date))),recent=all.filter(n=>new Date(n.date)>=weekAgo);return panel('Recent activity · last 7 days',`
${btn('Show all activity',"showAllActivityV17()")}
`+table(['Date','Activity'],recent.map(n=>row([new Date(n.date).toLocaleString('en-US'),esc(n.message)]))))} function showAllActivityV17(){modal('All activity history',table(['Date','Activity'],(db.notifications||[]).map(n=>row([new Date(n.date).toLocaleString('en-US'),esc(n.message)]))))} const dashBaseV17=dashboardV16;dashboardV16=function(){let html=dashBaseV17();html=html.replace(/

Recent activity<\/h2>[\s\S]*?<\/section>/,''); // Keep other dashboard panels intact. const todayBooked=db.lessons.filter(l=>l.date===today()&&l.status!=='Available'&&l.status!=='Cancelled');const dayButtons=panel('Today’s lessons · Manage',table(['Time','Athlete','Facility','Status','Action'],todayBooked.map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.status),cancelButtonV17(l)]))));const upcoming=db.lessons.filter(l=>l.date>today()&&l.status!=='Available'&&l.status!=='Cancelled').sort((a,b)=>(a.date+a.time).localeCompare(b.date+b.time)).slice(0,20);return html+dayButtons+panel('Upcoming lessons · Manage',table(['Time','Athlete','Facility','Status','Action'],upcoming.map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.status),cancelButtonV17(l)]))))+recentActivitiesV17()};views['Dashboard']=()=>role==='admin'?dashboardV16():role==='parent'?parentDash():athleteDashV9(); const v17style=document.createElement('style');v17style.textContent=`.calgrid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px;margin-top:14px}.calhead{text-align:center;color:var(--gold);font-weight:bold;padding:8px 0}.calday{min-height:88px!important;display:flex;flex-direction:column;align-items:flex-start;justify-content:space-between;padding:10px!important;border:1px solid #7775!important;border-radius:9px!important;background:var(--panel,#252c42)!important;color:inherit!important;cursor:pointer}.calday:hover{outline:2px solid var(--gold)}.calday small{font-size:11px;opacity:.85}.calblank{min-height:88px}.calitem{padding:12px;border-bottom:1px solid #7775}.calitem .actions{display:inline-flex;margin-left:10px}@media(max-width:650px){.calgrid{gap:2px}.calday{min-height:66px!important;padding:4px!important}.calday small{font-size:9px}}@media print{body.print-lesson-only *{visibility:hidden!important}body.print-lesson-only #printable-lesson-history,body.print-lesson-only #printable-lesson-history *{visibility:visible!important}body.print-lesson-only #printable-lesson-history{position:absolute;left:0;top:0;width:100%;color:#111!important;background:white!important}body.print-lesson-only #printable-lesson-history .no-print,body.print-lesson-only #printable-lesson-history button{display:none!important}body.print-lesson-only #printable-lesson-history table{width:100%;color:#111!important}}`;document.head.appendChild(v17style); // V18: linked local-demo records, working actions, and streamlined dashboard. function familyNoticeV18(parent,body,kind='lesson'){ db.familyNoticesV18 ||= []; db.familyNoticesV18.unshift({id:id(),parentId:parent,date:new Date().toISOString(),message:body,kind}); db.notifications ||= []; db.notifications.unshift({id:id(),date:new Date().toISOString(),message:body,parentId:parent,kind}); } function lessonActionsV18(l){return ``;} function deleteLessonV18(lid){if(role!=='admin')return;let l=db.lessons.find(x=>x.id===lid);if(!l||!confirm('Delete this lesson from all linked demo views without notifying the parent? Any associated payment remains in financial records for reconciliation.'))return;db.lessons=db.lessons.filter(x=>x.id!==lid);db.requests=db.requests.filter(x=>x.lessonId!==lid);db.notifications ||= [];db.notifications.unshift({id:id(),date:new Date().toISOString(),message:`Admin deleted a lesson record (${l.date}, ${aname(l.athleteId)}); no family notice issued.`});save();closeModal();render();} // The previous form helper ignores a fourth argument; attach the lesson ID to the actual form. lessonCancelV17=function(lid){if(role!=='admin')return;let l=db.lessons.find(x=>x.id===lid);if(!l||l.status==='Cancelled')return;modal('Cancel lesson · Notify family',`

${esc(aname(l.athleteId))} · ${esc(fmt(l.date,l.time))} · ${esc(l.facility)}

The notice appears in the linked parent's demo portal. Email and SMS require a deployed backend and messaging service.

`+form(sel('Offer to family','resolution',['Offer replacement date/time','Offer facility-specific Champions Credits','Offer refund','Discuss options with parent'])+txt('Message to parent','message','I need to cancel our scheduled lesson. I apologize for the inconvenience. We can arrange another date, apply a lesson credit, or discuss a refund.')+field('Proposed replacement date (optional)','date','','date')+field('Proposed replacement time (optional)','time','','time'),'saveCancelV18','Cancel lesson & notify parent in portal').replace('
l.date===d&&l.time===t&&l.status!=='Cancelled'))return alert('This time is already booked or posted.');let l={id:id(),date:d,time:t,facility:fac,duration:Number(val(f,'duration'))||30,price:Number(val(f,'price'))||facilityPrice(fac),status:'Available',athleteId:'',parentId:''};db.lessons.push(l);save();render();alert('Availability posted for '+fmt(d,t)+'. It is now visible in the parent booking calendar (for eligible facility accounts).');} function calendarPostV18(date){closeModal();go('Calendar & Availability');setTimeout(()=>{const f=document.getElementById('v18-post-form');if(f){f.elements.date.value=date;f.scrollIntoView({behavior:'smooth',block:'center'});f.elements.time.focus();}},50);} adminCalendarV17=function(){let available=db.lessons.filter(l=>l.status==='Available'),booked=db.lessons.filter(l=>!['Available','Cancelled'].includes(l.status));return calendarMonthV17(false)+panel('Post available lessons',`
${field('Date','date',today(),'date')}${field('Time','time','','time')}${sel('Facility','facility',['World Cup East Brunswick','Legacy Cheer'])}${field('Duration (minutes)','duration','30','number')}${field('Price ($)','price','55','number','min="0"')}
`)+panel(`Booked appointments (${booked.length})`,table(['Date & time','Athlete','Facility','Status','Price','Actions'],booked.map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.status),money(l.price),lessonActionsV18(l)]))))+panel(`Available appointments (${available.length})`,table(['Date & time','Facility','Price','Action'],available.map(l=>row([fmt(l.date,l.time),esc(l.facility),money(l.price),btn('Remove opening',`removeSlotV17('${l.id}')`)]))));};views['Calendar & Availability']=adminCalendarV17; calendarDayV17=function(date,isParent){let ls=db.lessons.filter(l=>l.date===date&&(isParent?l.status==='Available':true)).sort((a,b)=>a.time.localeCompare(b.time));modal('Appointments · '+date,`

${ls.length?ls.length+' appointment(s)':'No appointments posted for this date.'}

${ls.map(l=>`
${esc(fmt(l.date,l.time))} · ${esc(l.facility)} · ${esc(l.status)} · ${money(l.price)} ${isParent?btn('Book',`closeModal();bookSlot('${l.id}')`,true):l.status==='Available'?btn('Remove opening',`removeSlotV17('${l.id}')`):lessonActionsV18(l)}
`).join('')}
${isParent?btn('Request another time',`closeModal();requestLessonV17('${date}')`,true):btn('Post availability',`calendarPostV18('${date}')`,true)}
`);}; function dashboardV18(){let base=dashBaseV17();const cut=base.indexOf('

Today’s lessons');if(cut>=0){const end=base.indexOf('

',cut);if(end>=0)base=base.slice(0,cut)+base.slice(end+10);}base=base.replace(/

Recent activity<\/h2>[\s\S]*?<\/section>/,'');let todayList=db.lessons.filter(l=>l.date===today()&&!['Available','Cancelled'].includes(l.status));let upcoming=db.lessons.filter(l=>l.date>today()&&!['Available','Cancelled'].includes(l.status)).sort((a,b)=>(a.date+a.time).localeCompare(b.date+b.time));return base+panel('Today’s lessons',table(['Time','Athlete','Facility','Status','Actions'],todayList.map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.status),lessonActionsV18(l)]))))+panel('Upcoming lessons',table(['Time','Athlete','Facility','Status','Actions'],upcoming.map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.status),lessonActionsV18(l)]))))+recentActivitiesV17();} views['Dashboard']=()=>role==='admin'?dashboardV18():role==='parent'?parentDashV18():athleteDashV9(); function familyInboxV18(){let notices=(db.familyNoticesV18||[]).filter(n=>n.parentId===parentId||n.parentId==='ALL');return panel('Messages from Champions Core',notices.length?notices.map(n=>`
${esc(new Date(n.date).toLocaleString())}

${esc(n.message)}

`).join(''):'

No messages yet.

');} function parentDashV18(){return parentDash()+familyInboxV18();} views['Family Notices']=familyInboxV18;menus.parent.Family.push('Family Notices'); function massMessageV18(){return panel('Notify all parents',`

Choose a delivery channel. Portal messages work in this local demonstration. Email and text are drafts only until a secure delivery service is connected.

${sel('Send via','channel',[['portal','Parent accounts (portal)'],['email','Email (draft only)'],['sms','Text message (draft only)']])}${field('Subject','subject','Champions Core announcement')}${txt('Message','message','')}
`)+panel('Announcement history',table(['Date','Channel','Subject','Status'],(db.massMessagesV18||[]).map(n=>row([esc(new Date(n.date).toLocaleString()),esc(n.channel),esc(n.subject),esc(n.status)]))));} function sendMassV18(f){let channel=val(f,'channel'),subject=val(f,'subject'),message=val(f,'message');if(!message)return alert('Enter a message.');db.massMessagesV18 ||= [];let status=channel==='portal'?'Published to all demo parent accounts':'Draft only — not sent externally';db.massMessagesV18.unshift({id:id(),date:new Date().toISOString(),channel,subject,message,status});if(channel==='portal')familyNoticeV18('ALL',`${subject}: ${message}`,'announcement');save();render();alert(status);} menus.admin.People.push('Notify All Parents');views['Notify All Parents']=massMessageV18; // Parent's regular Messages menu includes coach announcements as well. const parentMessagesV18=views['Messages'];views['Messages']=()=>role==='parent'?familyInboxV18()+(parentMessagesV18?parentMessagesV18():''):parentMessagesV18?parentMessagesV18():''; const v18css=document.createElement('style');v18css.textContent=`select[aria-label="Lesson actions"]{min-width:175px;max-width:220px;padding:9px;border-radius:9px;cursor:pointer}`;document.head.appendChild(v18css); render(); // Version 19: coordinated demo deletion, athlete access requests, report browsing. function deleteLessonV18(lid){ if(role!=='admin')return; const l=db.lessons.find(x=>x.id===lid);if(!l)return; const linked=db.payments.filter(p=>p.lessonId===lid&&!p.voided); if(!confirm(`Delete this lesson from the coach, parent, and athlete previews? ${linked.length} associated payment(s) totaling ${money(linked.reduce((n,p)=>n+Number(p.amount||0),0))} will be removed from active income and parent payment history. This does NOT issue a real refund. No parent notification will be sent.`))return; db.lessons=db.lessons.filter(x=>x.id!==lid); db.requests=db.requests.filter(x=>x.lessonId!==lid); for(const p of linked){p.voided=true;p.voidReason='Lesson deleted by administrator';p.voidedAt=new Date().toISOString();} for(const x of db.income){if(x.lessonId===lid&&!x.voided){x.voided=true;x.voidReason='Lesson deleted by administrator';x.voidedAt=new Date().toISOString();}} db.notifications.unshift({id:id(),date:new Date().toISOString(),message:`Admin deleted lesson and voided ${linked.length} linked payment(s) for ${aname(l.athleteId)}; no family notice issued.`}); save();closeModal();render(); } function reportsV19(){ const selected=window.reportAthleteFilterV19||''; const choices=db.athletes.slice().sort((a,b)=>a.name.localeCompare(b.name)); const list=selected?db.reports.filter(r=>r.athleteId===selected).sort((a,b)=>String(b.date).localeCompare(String(a.date))):[]; return panel('Lesson reports by athlete',`

Choose an athlete to view their lesson reports. Reports are not displayed until an athlete is selected.

${btn('Create lesson report','editReport()',true)}
`+(selected?`

${list.length} report(s) for ${esc(aname(selected))}

`+table(['Date','Status','Notes','Actions'],list.map(r=>row([esc(r.date),esc(r.status),esc(r.notes).slice(0,90),btn('Open',`editReport('${r.id}')`)+' '+btn('Delete',`deleteReportV19('${r.id}')`)]))):'')); } function deleteReportV19(rid){if(role!=='admin')return;const r=db.reports.find(x=>x.id===rid);if(!r)return;if(!confirm(`Delete the ${r.date} lesson report for ${aname(r.athleteId)}? It will disappear from parent and athlete previews too.`))return;db.reports=db.reports.filter(x=>x.id!==rid);save();render();} views['Lesson Reports']=reportsV19; function athleteAccessParentV19(){ const mine=db.athletes.filter(a=>a.parentId===parentId); const requests=db.athleteAccessV19||[]; return panel('Athlete portal access',`

Request a separate athlete profile and login. Your coach must approve it before access is activated. This is a local preview; no actual invitation or secure login is sent.

`+table(['Athlete','Account request','Action'],mine.map(a=>{const r=requests.find(x=>x.athleteId===a.id);return row([esc(a.name),esc(r?.status||'Not requested'),!r?btn('Request athlete account',`requestAthleteAccessV19('${a.id}')`,true):r.status==='Denied'?btn('Request again',`requestAthleteAccessV19('${a.id}')`):'—'])})));} function requestAthleteAccessV19(aid){const a=db.athletes.find(x=>x.id===aid&&x.parentId===parentId);if(!a)return;modal('Request athlete portal account',`

${esc(a.name)} · Linked guardian: ${esc(pname(parentId))}

Coach approval is required. Athlete access will be limited to their own development information.

`+form(field('Athlete account email (demo)','email','','email','required')+field('Preferred athlete display name','displayName',a.name),'submitAthleteAccessV19','Submit for coach approval').replace('
x.athleteId!==a.id);db.athleteAccessV19.unshift({id:id(),athleteId:a.id,parentId,email,displayName:val(f,'displayName')||a.name,status:'Pending approval',requestedAt:new Date().toISOString()});notice(`Athlete account approval requested: ${a.name} (${pname(parentId)})`);save();closeModal();render();alert('Request sent to the coach approval queue in this demo.');} function athleteAccessAdminV19(){return panel('Athlete account approval requests',`

Approving enables the athlete preview profile; a real invitation email and separate secure login require a deployed authentication backend.

`+table(['Athlete','Guardian','Requested email','Status','Decision'],(db.athleteAccessV19||[]).map(r=>row([esc(aname(r.athleteId)),esc(pname(r.parentId)),esc(r.email),esc(r.status),r.status==='Pending approval'?btn('Approve',`decideAthleteAccessV19('${r.id}',true)`,true)+' '+btn('Deny',`decideAthleteAccessV19('${r.id}',false)`):'—']))));} function decideAthleteAccessV19(rid,approve){if(role!=='admin')return;const r=(db.athleteAccessV19||[]).find(x=>x.id===rid);if(!r)return;r.status=approve?'Approved':'Denied';r.decidedAt=new Date().toISOString();if(approve){const a=db.athletes.find(x=>x.id===r.athleteId);if(a)a.athletePortalApproved=true;}familyNoticeV18(r.parentId,`Athlete portal account request for ${aname(r.athleteId)}: ${r.status}.`);save();render();} menus.parent.Family.push('Athlete Portal Access');menus.admin.People.push('Athlete Account Approvals');views['Athlete Portal Access']=athleteAccessParentV19;views['Athlete Account Approvals']=athleteAccessAdminV19; // Do not expose athlete development preview until a linked guardian's request is approved. const oldAthleteDashboardV19=views['Dashboard']; views['Dashboard']=()=>role==='athlete'&&!db.athletes.find(a=>a.id===athleteId)?.athletePortalApproved?panel('Athlete account awaiting approval','A guardian must request an athlete account and the coach must approve it before this athlete can access the portal preview.'):oldAthleteDashboardV19(); render(); // V20: only active payments count, and demonstrate the separate registration/approval/login journey. function activePaymentsV20(){return db.payments.filter(p=>!p.voided&&db.lessons.some(l=>l.id===p.lessonId));} function activeIncomeV20(){return db.income.filter(x=>!x.voided&&(!x.lessonId||db.lessons.some(l=>l.id===x.lessonId)));} function incomeAllRecords(){return activePaymentsV20().map(p=>{const l=db.lessons.find(x=>x.id===p.lessonId);return {id:p.id,recordKind:'payment',date:l?.date||'',description:`Private lesson · ${aname(l?.athleteId)} · ${l?.facility||''}`,facility:l?.facility||'',amount:Number(p.amount||0),status:p.status||'Pending',method:p.method||'—',source:'Lesson payment',type:'Private lesson'}}).concat(activeIncomeV20().map(x=>({...x,recordKind:'income',amount:Number(x.amount||0),status:x.status||'Paid',method:x.method||'Manual',source:x.source||'Manual'})));} function financial(){const payments=activePaymentsV20(),pending=payments.filter(p=>p.status==='Awaiting verification');return panel('Payment verification queue',table(['Parent','Athlete','Amount','Method','Status','Action'],pending.map(p=>{const l=db.lessons.find(x=>x.id===p.lessonId);return row([esc(pname(p.parentId)),esc(aname(l?.athleteId)),money(p.amount),esc(p.method||'—'),esc(p.status),btn('Confirm received',`verify('${p.id}')`,true)])})))+panel('Financial snapshot',`
Confirmed lesson payments
${money(payments.filter(p=>p.status==='Confirmed').reduce((s,p)=>s+Number(p.amount||0),0))}
Expenses
${money(db.expenses.filter(e=>!e.voided).reduce((s,e)=>s+Number(e.amount||0),0))}
`)} function parentPayments(){const pay=activePaymentsV20().filter(p=>p.parentId===parentId);return panel('My payments and receipts',`
${btn('Export payment history (CSV)',"exportRows('payments')",true)}${btn('Print','window.print()')}
`+table(['Lesson','Amount','Method','Status','Action'],pay.map(p=>{const l=db.lessons.find(x=>x.id===p.lessonId);return row([l?fmt(l.date,l.time):'—',money(p.amount),esc(p.method||'Not selected'),esc(p.status),p.status==='Awaiting payment'?btn('Continue to payment',`paymentModal('${p.id}')`,true):'—'])})))} // Keep an audit archive, but never count archived entries as money received. const financialOverviewV20=views['Financial Overview']; views['Financial Overview']=()=>financialOverviewV20?financialOverviewV20():financial(); // Demo registration is deliberately local: it is NOT authentication and should use test data only. function demoSignupV20(){modal('Parent sign-up · test walkthrough',`
Local demonstration only. No email is sent and no secure login is created. Use fictional details.
`+form(field('Parent full name','name','','text','required')+field('Email address','email','','email','required')+field('Phone','phone','','tel')+sel('Facility','facility',db.facilities.map(x=>x.name)), 'saveDemoSignupV20','Submit parent account request'));} function saveDemoSignupV20(f){const name=val(f,'name').trim(),email=val(f,'email').trim();if(!name||!email)return alert('Name and email are required.');if(db.parents.some(p=>p.email?.toLowerCase()===email.toLowerCase()))return alert('This demo email already has a parent profile.');const p={id:id(),name,email,phone:val(f,'phone'),facility:val(f,'facility'),status:'Pending'};db.parents.push(p);db.notifications.unshift({id:id(),date:new Date().toISOString(),message:'New parent approval requested: '+name});save();closeModal();render();alert('Request saved. Switch to Admin → Parents & Guardians to approve the parent.');} function demoParentLoginV20(){modal('Parent login · local preview',`

Choose a previously approved demo parent. This is a role simulation, not a password-protected login.

`+form(sel('Approved parent','pid',db.parents.filter(p=>p.status==='Active').map(p=>[p.id,p.name+' — '+p.email])),'enterDemoParentV20','Enter parent preview'));} function enterDemoParentV20(f){const p=db.parents.find(x=>x.id===val(f,'pid')&&x.status==='Active');if(!p)return alert('Parent must be approved first.');parentId=p.id;closeModal();setRole('parent');} function approveDemoParentV20(pid){const p=db.parents.find(x=>x.id===pid);if(!p||role!=='admin')return;p.status='Active';familyNoticeV18(p.id,'Your parent account has been approved in this local demo.');save();render();} const oldParentsV20=views['Parents & Guardians']; views['Parents & Guardians']=()=>panel('Parent registration and access · demo',`
${btn('Test parent sign-up','demoSignupV20()',true)}${btn('Test parent login','demoParentLoginV20()')}

New parent requests must be approved before entering the parent preview. These are not real online accounts.

`+table(['Name','Email','Status','Action'],db.parents.map(p=>row([esc(p.name),esc(p.email),esc(p.status),p.status==='Pending'?btn('Approve',`approveDemoParentV20('${p.id}')`,true):'—']))))+(oldParentsV20?oldParentsV20():''); function demoAddAthleteV20(){const p=db.parents.find(x=>x.id===parentId);if(!p||p.status!=='Active')return alert('Parent approval required.');modal('Add another athlete · guardian request',form(field('Athlete full name','name','','text','required')+field('Date of birth','dob','','date')+field('Goals / notes','goal'),'saveDemoAthleteV20','Submit athlete for approval'));} function saveDemoAthleteV20(f){const p=db.parents.find(x=>x.id===parentId),name=val(f,'name').trim();if(!p||!name)return alert('Enter athlete name.');db.athleteRequestsV20 ||= [];db.athleteRequestsV20.unshift({id:id(),parentId:p.id,name,dob:val(f,'dob'),goal:val(f,'goal'),status:'Pending approval'});notice('New athlete profile request from '+p.name+': '+name);save();closeModal();render();alert('Athlete request submitted for coach approval in this demo.');} function decideAthleteProfileV20(rid,approve){if(role!=='admin')return;const r=(db.athleteRequestsV20||[]).find(x=>x.id===rid);if(!r)return;r.status=approve?'Approved':'Denied';if(approve)db.athletes.push({id:id(),name:r.name,parentId:r.parentId,facility:db.parents.find(p=>p.id===r.parentId)?.facility||'',level:'Beginner',status:'Active',dob:r.dob,goal:r.goal});familyNoticeV18(r.parentId,`Athlete profile request for ${r.name}: ${r.status}.`);save();render();} const oldAthleteApprovalsV20=views['Athlete Account Approvals']; views['Athlete Account Approvals']=()=>panel('New athlete profile approvals',table(['Athlete','Guardian','Status','Action'],(db.athleteRequestsV20||[]).map(r=>row([esc(r.name),esc(pname(r.parentId)),esc(r.status),r.status==='Pending approval'?btn('Approve',`decideAthleteProfileV20('${r.id}',true)`,true)+' '+btn('Deny',`decideAthleteProfileV20('${r.id}',false)`):'—']))))+(oldAthleteApprovalsV20?oldAthleteApprovalsV20():''); const oldAccessParentV20=views['Athlete Portal Access']; views['Athlete Portal Access']=()=>panel('Manage my athletes',`

You can request multiple athlete profiles. Once the coach approves each profile, request separate athlete portal access below.

${btn('Add another athlete','demoAddAthleteV20()',true)}`)+(oldAccessParentV20?oldAccessParentV20():''); // Prevent demo role selection from being mistaken for real authentication. const demoCSSV20=document.createElement('style');demoCSSV20.textContent='@media print{aside,.top,.actions,.footer,button,select{display:none!important}.shell{display:block}main{padding:0!important}body{background:white!important;color:black!important}.panel{break-inside:avoid}}';document.head.appendChild(demoCSSV20); // V21: show pending athlete profile and separate athlete-login requests directly on admin dashboard. const dashboardBeforeV21=views['Dashboard']; views['Dashboard']=()=>{ if(role!=='admin')return dashboardBeforeV21(); const profiles=(db.athleteRequestsV20||[]).filter(r=>r.status==='Pending approval'); const accounts=(db.athleteAccessV19||[]).filter(r=>r.status==='Pending approval'); const total=profiles.length+accounts.length; const details=total?table(['Request','Athlete','Guardian','Action'],[ ...profiles.map(r=>row(['New athlete profile',esc(r.name),esc(pname(r.parentId)),btn('Review',"go('Athlete Account Approvals')",true)])), ...accounts.map(r=>{const a=db.athletes.find(x=>x.id===r.athleteId);return row(['Athlete login access',esc(a?.name||'Athlete'),esc(pname(r.parentId)),btn('Review',"go('Athlete Account Approvals')",true)]);}) ]):'

No athlete account requests awaiting approval.

'; return panel('Athlete account requests · '+total+' pending',`
${btn('Review athlete approvals',"go('Athlete Account Approvals')",total>0)}
${details}`)+dashboardBeforeV21(); }; // V22: keep guardian profile requests, coach approvals, and athlete preview in sync in this browser. const accessParentBeforeV22=views['Athlete Portal Access']; views['Athlete Portal Access']=()=>{ const pending=(db.athleteRequestsV20||[]).filter(r=>r.parentId===parentId); const rows=pending.map(r=>row([esc(r.name),esc(r.status),esc(r.status==='Approved'?'Profile approved — request a separate athlete login below.':r.status==='Pending approval'?'Awaiting coach approval':'Contact your coach')])); return panel('Athlete profile requests · '+pending.length,`

Each submitted athlete appears here immediately. Profile approval and separate athlete login approval are two steps.

`+table(['Athlete','Profile status','Next step'],rows))+accessParentBeforeV22(); }; const myAthletesBeforeV22=views['My Athletes']; views['My Athletes']=()=>{ const requests=(db.athleteRequestsV20||[]).filter(r=>r.parentId===parentId); return panel('My athlete profile requests',table(['Athlete','Approval status'],requests.map(r=>row([esc(r.name),esc(r.status)]))))+(myAthletesBeforeV22?myAthletesBeforeV22():''); }; // A coach-approved profile does not automatically confer separate athlete-login permission. function athletePreviewSwitcherV22(){ const entries=[...db.athletes.map(a=>({key:a.id,label:a.name,approved:!!a.athletePortalApproved})),...(db.athleteRequestsV20||[]).filter(r=>r.status==='Pending approval').map(r=>({key:'request:'+r.id,label:r.name+' (profile pending)',approved:false}))]; const selected=window.athletePreviewSelectionV22||athleteId; return panel('Athlete account preview · select athlete',`

This is a simulated view, not a secure athlete login.

`); } const dashboardBeforeV22=views['Dashboard']; views['Dashboard']=()=>{ if(role!=='athlete')return dashboardBeforeV22(); const selected=window.athletePreviewSelectionV22||athleteId; if(selected.startsWith('request:')){ const req=(db.athleteRequestsV20||[]).find(r=>'request:'+r.id===selected); if(req?.status==='Approved'){ const a=db.athletes.find(x=>x.profileRequestId===req.id);if(a){window.athletePreviewSelectionV22=a.id;return views['Dashboard']();} } return athletePreviewSwitcherV22()+panel('Profile pending coach approval',`${esc(req?.name||'Athlete')}

Status: ${esc(req?.status||'Not found')}

The coach must approve this athlete profile before a separate athlete account can be requested.

`); } const a=db.athletes.find(x=>x.id===selected); if(!a)return athletePreviewSwitcherV22()+panel('Athlete not found','Choose an athlete above.'); athleteId=a.id; return athletePreviewSwitcherV22()+(a.athletePortalApproved?dashboardBeforeV22():panel('Athlete account awaiting approval',`${esc(a.name)}

Status: ${((db.athleteAccessV19||[]).find(r=>r.athleteId===a.id)?.status)||'Not requested'}

The guardian must request a separate athlete login and the coach must approve it.

`)); }; const approveProfileBeforeV22=decideAthleteProfileV20; decideAthleteProfileV20=function(rid,approve){ const r=(db.athleteRequestsV20||[]).find(x=>x.id===rid); approveProfileBeforeV22(rid,approve); if(r&&approve){const a=db.athletes.find(x=>x.parentId===r.parentId&&x.name===r.name&&x.dob===r.dob&&!x.profileRequestId);if(a){a.profileRequestId=rid;save();}} if(r&&window.athletePreviewSelectionV22==='request:'+rid)render(); }; // In this single-file demo, changes are shared across preview roles in the same browser only. window.addEventListener('storage',e=>{if(e.key===KEY&&e.newValue){try{db=JSON.parse(e.newValue);render()}catch{}}}); // Version 24: central profile photos, keyed by account ID. Stored only in this browser. db.profilePhotosV24 ||= {admin:null,parents:{},athletes:{}}; function ccPhotoRecordV24(type,personId){ const p=db.profilePhotosV24||{}; return type==='admin'?p.admin:(p[type]||{})[personId]||null; } function ccInitialsV24(name){return String(name||'?').trim().split(/\s+/).slice(0,2).map(s=>s[0]||'').join('').toUpperCase()||'?';} function ccAvatarV24(type,personId,name,large=false){ const photo=ccPhotoRecordV24(type,personId); const cls='cc-avatar'+(large?' lg':''); return photo?`${esc(name)} profile photo`:`${esc(ccInitialsV24(name))}`; } function ccPersonV24(type,personId,name){return `${ccAvatarV24(type,personId,name)}${esc(name||'Unknown')}`;} function ccAthleteV24(aid){const a=db.athletes.find(x=>x.id===aid);return ccPersonV24('athletes',aid,a?.name||'Unknown athlete');} function ccParentV24(pid){const p=db.parents.find(x=>x.id===pid);return ccPersonV24('parents',pid,p?.name||'Unknown guardian');} function ccProfileCardV24(type,pid,name,editable){return `
${ccAvatarV24(type,pid,name,true)}
${esc(name)}

${type==='admin'?'Coach':type==='parents'?'Parent / guardian':'Athlete'} profile

${editable?`
${btn('Upload / change photo',`ccPickPhotoV24('${type}','${pid}')`,true)}${ccPhotoRecordV24(type,pid)?btn('Remove photo',`ccRemovePhotoV24('${type}','${pid}')`):''}
`:''}
`;} function ccPhotoPermissionV24(type,pid){ if(type==='admin')return role==='admin'; if(type==='parents')return role==='admin'||(role==='parent'&&pid===parentId); if(type==='athletes'){const a=db.athletes.find(x=>x.id===pid);return role==='admin'||(role==='parent'&&a?.parentId===parentId)||(role==='athlete'&&pid===athleteId&&!!a?.athletePortalApproved);} return false; } function ccPickPhotoV24(type,pid){ if(!ccPhotoPermissionV24(type,pid))return alert('You do not have permission to edit this photo in the preview.'); const input=document.createElement('input');input.type='file';input.accept='image/png,image/jpeg,image/webp'; input.onchange=()=>{const file=input.files?.[0];if(!file)return;if(!file.type.startsWith('image/'))return alert('Choose an image file.');if(file.size>12*1024*1024)return alert('Please choose an image under 12 MB.'); const reader=new FileReader();reader.onerror=()=>alert('Unable to read that image.');reader.onload=()=>{ const img=new Image();img.onerror=()=>alert('Unable to open that image.');img.onload=()=>{ const c=document.createElement('canvas');c.width=220;c.height=220;const ctx=c.getContext('2d');const s=Math.min(img.width,img.height);ctx.drawImage(img,(img.width-s)/2,(img.height-s)/2,s,s,0,0,220,220); const data=c.toDataURL('image/jpeg',0.73);db.profilePhotosV24 ||= {admin:null,parents:{},athletes:{}}; if(type==='admin')db.profilePhotosV24.admin=data;else (db.profilePhotosV24[type] ||= {})[pid]=data; try{save();render();}catch(e){alert('Browser photo storage is full. Try removing a photo.');} };img.src=reader.result; };reader.readAsDataURL(file); };input.click(); } function ccRemovePhotoV24(type,pid){if(!ccPhotoPermissionV24(type,pid))return;if(!confirm('Remove this profile picture from all linked preview views?'))return; if(type==='admin')db.profilePhotosV24.admin=null;else delete db.profilePhotosV24[type]?.[pid];save();render();} function ccPhotoSettingsV24(){ if(role==='admin')return panel('My coach profile',ccProfileCardV24('admin','coach','Champions Core Coach',true)+'

Your photo appears beside coach communications and published lesson reports.

'); if(role==='parent')return panel('My parent profile',ccProfileCardV24('parents',parentId,pname(parentId),true)+'

Your photo appears beside your account and family communications.

'); const a=db.athletes.find(x=>x.id===athleteId); return panel('My athlete profile',a?ccProfileCardV24('athletes',athleteId,a.name,ccPhotoPermissionV24('athletes',athleteId)):'No athlete selected.'); } for(const r of ['admin','parent','athlete']){ const group=r==='admin'?'Administration':r==='parent'?'Family':'My Development'; if(!menus[r][group].includes('Profile Pictures'))menus[r][group].push('Profile Pictures'); } views['Profile Pictures']=ccPhotoSettingsV24; const ccOldAthletesV24=views['Athletes']; views['Athletes']=()=>role!=='admin'?ccOldAthletesV24():panel('Athlete profiles & pictures',`

Upload an athlete photo here, or let their guardian upload it in the parent portal. All previews in this browser use the same image.

${btn('Add athlete','editAthlete()',true)}`+db.athletes.map(a=>ccProfileCardV24('athletes',a.id,a.name,true)+`
${btn('Edit athlete',`editAthlete('${a.id}')`)}Guardian: ${esc(pname(a.parentId))}
`).join('')); const ccOldMyAthletesV24=views['My Athletes']; views['My Athletes']=()=>panel('Family profile pictures',`

You can upload or replace photos for athletes linked to your parent account.

`+ccProfileCardV24('parents',parentId,pname(parentId),true)+db.athletes.filter(a=>a.parentId===parentId).map(a=>ccProfileCardV24('athletes',a.id,a.name,true)).join(''))+(ccOldMyAthletesV24?ccOldMyAthletesV24():''); const ccOldParentsV24=views['Parents & Guardians']; views['Parents & Guardians']=()=>panel('Guardian profile pictures',db.parents.map(p=>ccProfileCardV24('parents',p.id,p.name,true)).join(''))+(ccOldParentsV24?ccOldParentsV24():''); const ccOldReportsV24=views['Lesson Reports']; views['Lesson Reports']=()=>panel('Coach & athlete report identities',`

Profile photos here always reflect the current account photo.

`+(db.reports||[]).map(r=>`
${ccAvatarV24('admin','coach','Coach')}
Coach → ${ccAthleteV24(r.athleteId)}

${esc(r.date||'')} · ${esc(r.status||'')}

${esc(r.notes||'')}

`).join('')||'

No lesson reports yet.

')+(ccOldReportsV24?ccOldReportsV24():''); const ccOldProgressV24=views['Progress & Achievements']; views['Progress & Achievements']=()=>panel('Coaching notes with profile photos',(db.reports||[]).filter(r=>r.status==='Published to family'&&db.athletes.some(a=>a.id===r.athleteId&&a.parentId===parentId)).map(r=>`
${ccAvatarV24('admin','coach','Coach')}
Coach → ${ccAthleteV24(r.athleteId)}

${esc(r.date||'')}

${esc(r.notes||'')}

${esc(r.next||'')}

`).join('')||'

No published coaching notes yet.

')+(ccOldProgressV24?ccOldProgressV24():''); const ccOldMessagesV24=views['Messages']; views['Messages']=()=>{ if(role==='admin')return panel('Conversations · account photos',(db.messages||[]).map(m=>`
${ccAvatarV24('admin','coach','Coach')}
Coach → ${ccParentV24(m.parentId)}

${esc(m.subject||'Message')} · ${esc(m.channel||'Portal')}

${esc(m.body||'')}

`).join('')||'

No messages yet.

')+(ccOldMessagesV24?ccOldMessagesV24():''); if(role==='parent')return panel('Messages & sender pictures',`

Coach and parent photos follow their accounts.

`+(db.familyNoticesV18||[]).filter(n=>n.parentId===parentId||n.parentId==='ALL').map(n=>`
${ccAvatarV24('admin','coach','Coach')}
Champions Core Coach

${esc(new Date(n.date).toLocaleString())}

${esc(n.message||'')}

`).join('')+(db.messages||[]).filter(m=>m.parentId===parentId).map(m=>`
${ccAvatarV24('admin','coach','Coach')}
Coach → ${ccParentV24(parentId)}

${esc(m.subject||'Message')}

${esc(m.body||'')}

`).join('')||'

No direct messages yet.

')+(ccOldMessagesV24?ccOldMessagesV24():''); return ccOldMessagesV24?ccOldMessagesV24():''; }; const ccOldDashboardV24=views['Dashboard']; views['Dashboard']=()=>{ let banner=''; if(role==='admin')banner=panel('Coach account',ccProfileCardV24('admin','coach','Champions Core Coach',false)); if(role==='parent')banner=panel('Parent account',ccProfileCardV24('parents',parentId,pname(parentId),false)); if(role==='athlete'){ const a=db.athletes.find(x=>x.id===athleteId); if(a)banner=panel('Athlete identity',ccProfileCardV24('athletes',a.id,a.name,false)); } return banner+ccOldDashboardV24(); }; // Athlete photos are not security credentials; all preview roles remain demonstrative. render(); // Mobile navigation: collapsed by default, expandable with accessible menu button. function toggleMobileMenu(force){ const sidebar=document.querySelector('aside'), toggle=document.getElementById('mobileMenuToggle'); if(!sidebar||!toggle)return; const open=typeof force==='boolean'?force:!sidebar.classList.contains('mobile-menu-open'); sidebar.classList.toggle('mobile-menu-open',open); toggle.setAttribute('aria-expanded',String(open)); toggle.innerHTML=open?' Close menu':' Menu'; } const originalGoMobileV23=go; go=function(x){originalGoMobileV23(x);if(window.matchMedia('(max-width:800px)').matches)toggleMobileMenu(false)}; window.addEventListener('keydown',e=>{if(e.key==='Escape')toggleMobileMenu(false)}); // v25: Photo editing only inside Settings or a deliberately opened account profile. let ccOpenProfileV25=null; function ccViewProfileV25(type,pid){ccOpenProfileV25={type,pid};go('Account Profile');} views['Account Profile']=()=>{ if(!ccOpenProfileV25)return panel('Account profile','Open an athlete or parent profile first.'); const {type,pid}=ccOpenProfileV25; const person=type==='athletes'?db.athletes.find(a=>a.id===pid):db.parents.find(p=>p.id===pid); if(!person)return panel('Account profile','This account is no longer available.'); if(role==='parent'&&type==='athletes'&&person.parentId!==parentId)return panel('Account profile','Access denied.'); if(role==='parent'&&type==='parents'&&pid!==parentId)return panel('Account profile','Access denied.'); if(role==='athlete'&&(type!=='athletes'||pid!==athleteId))return panel('Account profile','Access denied.'); return panel('Account profile',ccProfileCardV24(type,pid,person.name,ccPhotoPermissionV24(type,pid))+'

Photo changes are available only on this account profile or under Profile Pictures in settings.

'+btn('Back',role==='admin'?"go('Athletes')":role==='parent'?"go('My Athletes')":"go('Dashboard')")); }; views['Athletes']=()=>role!=='admin'?ccOldAthletesV24():panel('Athlete accounts',`

Open an athlete profile to manage their photo.

${btn('Add athlete','editAthlete()',true)}`+cc120DependentAthletes().map(a=>`
${ccPersonV24('athletes',a.id,a.name)}Guardian: ${esc(pname(a.parentId))}
${btn('Open profile',`ccViewProfileV25('athletes','${a.id}')`,true)}${btn('Edit details',`editAthlete('${a.id}')`)}
`).join('')); views['Parents & Guardians']=()=>panel('Parent & guardian accounts',`

Open a parent profile to manage their photo.

`+db.parents.map(p=>`
${ccPersonV24('parents',p.id,p.name)}${btn('Open profile',`ccViewProfileV25('parents','${p.id}')`,true)}
`).join(''))+(ccOldParentsV24?ccOldParentsV24():''); views['My Athletes']=()=>panel('My family profiles',`

Open a profile to change its picture.

${ccPersonV24('parents',parentId,pname(parentId))}${btn('Open my profile',`ccViewProfileV25('parents','${parentId}')`,true)}
`+db.athletes.filter(a=>a.parentId===parentId).map(a=>`
${ccPersonV24('athletes',a.id,a.name)}${btn('Open profile',`ccViewProfileV25('athletes','${a.id}')`,true)}
`).join(''))+(ccOldMyAthletesV24?ccOldMyAthletesV24():''); // v26: Restrict photo editing to profile/settings views, including on mobile. const ccOriginalPickV26=ccPickPhotoV24; ccPickPhotoV24=function(type,pid){if(!['Profile Pictures','Account Profile'].includes(page)){alert('Open the account profile or Settings → Profile Pictures to change photos.');return;}return ccOriginalPickV26(type,pid);}; const ccOriginalRemoveV26=ccRemovePhotoV24; ccRemovePhotoV24=function(type,pid){if(!['Profile Pictures','Account Profile'].includes(page)){alert('Open the account profile or Settings → Profile Pictures to remove photos.');return;}return ccOriginalRemoveV26(type,pid);}; views['Parents & Guardians']=()=>panel('Parent & guardian accounts',`

Select Open profile to edit an account photo.

`+db.parents.map(p=>`
${ccPersonV24('parents',p.id,p.name)}${btn('Open profile',`ccViewProfileV25('parents','${p.id}')`,true)}
`).join(''))+(ccOldParentsV24?ccOldParentsV24():''); views['My Athletes']=()=>panel('Family profiles',`

Select Open profile to edit a photo.

${ccPersonV24('parents',parentId,pname(parentId))}${btn('Open my profile',`ccViewProfileV25('parents','${parentId}')`,true)}
`+db.athletes.filter(a=>a.parentId===parentId).map(a=>`
${ccPersonV24('athletes',a.id,a.name)}${btn('Open profile',`ccViewProfileV25('athletes','${a.id}')`,true)}
`).join(''))+(ccOldMyAthletesV24?ccOldMyAthletesV24():''); // v27: the same hamburger control operates on desktop and mobile for every role. function ccMenuIsMobileV27(){return window.matchMedia('(max-width:800px)').matches} function ccMenuSetV27(open){ const aside=document.querySelector('aside'),toggle=document.getElementById('mobileMenuToggle'); if(!aside||!toggle)return; aside.classList.toggle('mobile-menu-open',ccMenuIsMobileV27()&&open); document.body.classList.toggle('cc-desktop-nav-open',!ccMenuIsMobileV27()&&open); toggle.setAttribute('aria-expanded',String(!!open)); toggle.setAttribute('aria-label',open?'Close navigation menu':'Open navigation menu'); toggle.innerHTML=open?' Close menu':' Menu'; } toggleMobileMenu=function(force){ const expanded=ccMenuIsMobileV27()?document.querySelector('aside').classList.contains('mobile-menu-open'):document.body.classList.contains('cc-desktop-nav-open'); ccMenuSetV27(typeof force==='boolean'?force:!expanded); }; const ccPreviousGoV27=go; go=function(x){ const wasMobile=ccMenuIsMobileV27(); const previousPage=page; ccPreviousGoV27(x); // Mobile: close only when navigating to another page, not on a background refresh. // Desktop: keep the navigation drawer open until the user closes it. if(wasMobile&&x!==previousPage)ccMenuSetV27(false); }; let ccMenuBreakpointV27=ccMenuIsMobileV27(); window.addEventListener('resize',()=>{ // Mobile Safari resizes the viewport when its browser chrome changes. // Never close the menu unless the actual mobile/desktop breakpoint changes. const next=ccMenuIsMobileV27(); if(next!==ccMenuBreakpointV27){ccMenuBreakpointV27=next;ccMenuSetV27(false);} }); ccMenuSetV27(false); // v28: persistent iOS-compatible image picker; status messages and safe storage checks. function ccPhotoMessageV28(message,error=false){ let box=document.getElementById('cc-photo-feedback-v28'); if(!box){box=document.createElement('div');box.id='cc-photo-feedback-v28';box.setAttribute('role','status');box.style.cssText='position:fixed;bottom:20px;left:12px;right:12px;max-width:520px;margin:auto;padding:14px 18px;border-radius:12px;z-index:999999;font-weight:700;box-shadow:0 6px 30px #0009';document.body.appendChild(box)} box.style.background=error?'#732b31':'#20553b';box.style.color='white';box.textContent=message; clearTimeout(window.ccPhotoMessageTimerV28);window.ccPhotoMessageTimerV28=setTimeout(()=>box.remove(),6500); } function ccPhotoInputV28(){ let input=document.getElementById('cc-photo-picker-v28'); if(!input){input=document.createElement('input');input.id='cc-photo-picker-v28';input.type='file';input.accept='image/*,.heic,.heif';input.setAttribute('aria-label','Choose profile picture');input.style.cssText='position:fixed;left:-10000px;top:0;opacity:0;width:1px;height:1px';document.body.appendChild(input)} return input; } ccPickPhotoV24=function(type,pid){ if(!['Profile Pictures','Account Profile'].includes(page)){ccPhotoMessageV28('Open Settings or an account profile to edit photos.',true);return} if(!ccPhotoPermissionV24(type,pid)){ccPhotoMessageV28('You do not have permission to edit this photo.',true);return} const input=ccPhotoInputV28();input.value=''; input.onchange=async function(){ const file=input.files&&input.files[0];if(!file)return; if(file.size>20*1024*1024){ccPhotoMessageV28('Choose a photo smaller than 20 MB.',true);return} ccPhotoMessageV28('Processing photo…'); let objectUrl=''; try{ objectUrl=URL.createObjectURL(file); const img=await new Promise((resolve,reject)=>{const im=new Image();im.onload=()=>resolve(im);im.onerror=()=>reject(new Error('This photo format could not be decoded. On iPhone, try saving it as JPEG or taking a screenshot.'));im.src=objectUrl}); const side=Math.min(img.naturalWidth,img.naturalHeight); if(!side)throw new Error('The selected photo is empty.'); const canvas=document.createElement('canvas');canvas.width=160;canvas.height=160; const ctx=canvas.getContext('2d');if(!ctx)throw new Error('Photo editing is not supported in this browser.'); ctx.drawImage(img,(img.naturalWidth-side)/2,(img.naturalHeight-side)/2,side,side,0,0,160,160); const data=canvas.toDataURL('image/jpeg',0.58); if(!data.startsWith('data:image/jpeg'))throw new Error('The browser could not convert the selected photo.'); db.profilePhotosV24 ||= {admin:null,parents:{},athletes:{}}; const previous=ccPhotoRecordV24(type,pid); if(type==='admin')db.profilePhotosV24.admin=data;else (db.profilePhotosV24[type] ||= {})[pid]=data; try{save()}catch(e){if(type==='admin')db.profilePhotosV24.admin=previous;else if(previous)db.profilePhotosV24[type][pid]=previous;else delete db.profilePhotosV24[type][pid];throw new Error('This browser cannot save the photo. Its local storage may be full or restricted.')} render();ccPhotoMessageV28('Profile photo saved on this device.'); }catch(err){ccPhotoMessageV28(err.message||'Unable to upload the photo.',true)}finally{if(objectUrl)URL.revokeObjectURL(objectUrl);input.value=''} }; input.click(); }; /* Version 29 — streamlined role-specific navigation */ menus.parent={ 'Dashboard':['Dashboard'], 'Scheduling':['Book a Lesson','My Lessons'], 'Payments & Credits':['My Payments','My Credits'], 'Athletes & Progress':['My Athletes','Progress & Achievements','Lesson Reports'], 'Communication':['Messages','Family Notices'], 'Account & Family':['Athlete Portal Access','My Agreements','Profile Pictures'] }; menus.athlete={ 'Dashboard':['Dashboard'], 'My Training':['My Lessons','My Goals','My Skills'], 'My Progress':['My Achievements','My Lesson Reports','My Coaching Notes','My Media'], 'My Account':['Profile Pictures'] }; // Keep only valid, unique destination pages and open the current section. renderNav=function(){ let out=''; for(const [group,items] of Object.entries(menus[role]||{})){ const unique=[...new Set(items)]; const active=unique.includes(page); out+=`
${esc(group)}${unique.map(x=>``).join('')}
`; } $('nav').innerHTML=out; }; /* V30 — per-parent cash eligibility; all payment methods remain available. Preview only. Never confirms actual receipt automatically. */ function ccCashEligibleV30(pid){ return !!db.parents.find(p=>p.id===pid)?.cashApproved; } editParent=function(pid=''){ if(role!=='admin')return; const p=db.parents.find(x=>x.id===pid)||{name:'',email:'',phone:'',facility:'World Cup East Brunswick',status:'Pending',cashApproved:false}; const facilities=(db.facilities||[]).map(f=>[f.name,f.name]); const cashControl=`
`; const fields=field('Full name','name',p.name||'')+ field('Email','email',p.email||'','email')+ field('Phone','phone',p.phone||'','tel')+ sel('Assigned facility','facility',facilities.length?facilities:['World Cup East Brunswick','Legacy Cheer'],p.facility)+ sel('Status','status',['Pending','Active','Suspended','Archived'],p.status)+cashControl; modal(pid?'Parent profile — '+esc(p.name):'Add parent', `
${fields}
${btn('Cancel','closeModal()')}
`); }; saveParent=function(f){ if(role!=='admin')return; let p=db.parents.find(x=>x.id===f.dataset.id); if(!p){p={id:id()};db.parents.push(p)} for(const k of ['name','email','phone','facility','status'])p[k]=val(f,k); p.cashApproved=!!f.elements.cashApproved?.checked; save();closeModal();render(); }; paymentModal=function(pid){ const p=db.payments.find(x=>x.id===pid&&!x.voided); if(!p)return; const l=db.lessons.find(x=>x.id===p.lessonId); if(!l)return; if(role==='parent'&&p.parentId!==parentId)return; if(role==='athlete')return; if(l.requestId&&!p.started){p.deadline=Date.now()+300000;p.started=true;save()} const cash=ccCashEligibleV30(p.parentId); const options=cc73PaymentOptions(); if(cash)options.push(['Cash','Pay coach in person — administrator confirms receipt']); const deadline=p.deadline||Date.now()+300000; modal('Lesson payment — DEMONSTRATION', `
DEMO ONLY — do not send real money. No payment is collected here.

${esc(aname(l.athleteId))} · ${fmt(l.date,l.time)}

${money(p.amount)} due

Time remaining to submit payment choice: 05:00

${options.map(([name,desc],i)=>``).join('')}

${cash?'Cash is enabled for this family. Selecting cash does not mark the lesson as paid.':'Cash is not enabled for this family. Ask the coach to authorize cash payments.'} All methods require administrator verification.

${btn('Submit payment choice (demo)',`sentPayment('${p.id}')`,true)}${btn('Close','closeModal()')}
`); const timer=setInterval(()=>{ const el=document.getElementById('countdown'); if(!el){clearInterval(timer);return} const sec=Math.max(0,Math.ceil((deadline-Date.now())/1000)); el.textContent=String(Math.floor(sec/60)).padStart(2,'0')+':'+String(sec%60).padStart(2,'0'); if(!sec&&p.status==='Awaiting payment'){ p.status='Expired';l.status='Available';l.parentId='';l.athleteId=''; save();clearInterval(timer);closeModal();render(); alert('Payment selection window expired. The slot was released (demo).'); } },1000); }; sentPayment=function(pid){ const p=db.payments.find(x=>x.id===pid&&!x.voided); if(!p)return; if(role==='parent'&&p.parentId!==parentId)return; if(role==='athlete')return; if(p.deadline&&Date.now()>p.deadline){closeModal();return alert('Payment selection window expired.')} const method=document.querySelector('input[name="paymethod"]:checked')?.value||'Cash App'; if(method==='Cash'&&!ccCashEligibleV30(p.parentId))return alert('Cash payments are not authorized for this parent.'); p.method=method;p.status='Awaiting verification'; const l=db.lessons.find(x=>x.id===p.lessonId); if(l)l.status='Awaiting verification'; notice((method==='Cash'?'Cash payment pending admin confirmation: ':'Payment awaiting verification: ')+ aname(l?.athleteId)+' — '+money(p.amount)); save();closeModal();render(); alert(method==='Cash' ?'Cash selected. This is NOT marked paid. The coach must confirm receiving the cash (demo).' :'Payment submitted for coach verification (demo).'); }; /* v31 — editable guardian profiles and admin-only cash permission. */ views['Account Profile']=()=>{ if(!ccOpenProfileV25)return panel('Account profile','Open a parent or athlete profile first.'); const {type,pid}=ccOpenProfileV25; const person=(type==='athletes'?db.athletes:db.parents).find(x=>x.id===pid); if(!person)return panel('Account profile','This account is no longer available.'); if(role==='parent'&&((type==='athletes'&&person.parentId!==parentId)||(type==='parents'&&pid!==parentId)))return panel('Account profile','Access denied.'); if(role==='athlete'&&(type!=='athletes'||pid!==athleteId))return panel('Account profile','Access denied.'); const isParent=type==='parents'; const cash=isParent?`
Cash payment eligibility

${person.cashApproved?'Cash payments are authorized for this family. Cash will be available alongside other payment methods. Each payment still requires coach confirmation.':'Cash payments are not enabled for this family. Cash App, Venmo and Zelle remain available.'}

${role==='admin'?btn('Edit cash permission',`editParent('${pid}')`):'Only the coach can change this setting.'}
`:''; const info=isParent?`

Parent & guardian details

Full name
${esc(person.name||'—')}
Email
${esc(person.email||'—')}
Phone
${esc(person.phone||'—')}
Assigned facility
${esc(person.facility||'—')}
Account status
${esc(person.status||'—')}
Linked athletes
${esc(db.athletes.filter(a=>a.parentId===pid).map(a=>a.name).join(', ')||'None')}
${role==='admin'?btn('Edit parent details',`editParent('${pid}')`,true):''}
`:''; const athleteEdit=!isParent&&role==='admin'?`
${btn('Edit athlete details',`editAthlete('${pid}')`,true)}
`:''; const back=role==='admin'?(isParent?'Parents & Guardians':'Athletes'):(role==='parent'?'My Athletes':'Dashboard'); return panel('Account profile',ccProfileCardV24(type,pid,person.name,ccPhotoPermissionV24(type,pid))+info+cash+athleteEdit+`
${btn('Back',`go('${back}')`)}
`); }; views['Parents & Guardians']=()=>role!=='admin'?panel('Parent accounts','Access denied.'):panel('Parent & guardian accounts', `

Open a profile to see and edit all parent details, authorize cash payments, or change a photo.

`+ btn('Add parent',`editParent()`,true)+ db.parents.map(p=>`
${ccPersonV24('parents',p.id,p.name)}${esc(p.email||'')} · Cash: ${p.cashApproved?'Authorized':'Not authorized'}
${btn('Open profile',`ccViewProfileV25('parents','${p.id}')`,true)}${btn('Edit details / cash',`editParent('${p.id}')`)}
`).join('')+ (ccOldParentsV24?ccOldParentsV24():'')); // Preserve the currently open parent profile after saving an edit. const ccSaveParentV31=saveParent; saveParent=function(f){ const existing=f.dataset.id; ccSaveParentV31(f); if(existing&&ccOpenProfileV25?.type==='parents'&&ccOpenProfileV25.pid===existing){page='Account Profile';render()} }; /* V120: Directory membership is based on classification/record IDs, never names. Legacy synthetic billing contacts remain in local data for preview compatibility, but they are not Parent/Guardian accounts. */ function cc120IsIndividualAthlete(a){ if(!a)return false; return a.id==='a-independent-v50'||a.parentId==='p-independent-v50'||a.independentAccount===true||a.isIndependent===true||a.independent===true||['individual_athlete','independent'].includes(String(a.accountType||a.account_type||'').toLowerCase()); } function cc120DependentAthletes(){return (db.athletes||[]).filter(a=>!cc120IsIndividualAthlete(a));} function cc120GuardianParents(){ const contactIds=new Set((db.athletes||[]).filter(cc120IsIndividualAthlete).map(a=>a.parentId).filter(Boolean)); return (db.parents||[]).filter(p=>p.id!=='p-independent-v50'&&!p.independentAccount&&!contactIds.has(p.id)); } /* v32 — single streamlined guardian directory, no duplicate legacy panels. */ views['Parents & Guardians']=()=>{ if(role!=='admin')return panel('Parent accounts','Access denied.'); return panel('Parent & guardian accounts', `

Manage parent details, linked athletes, account status, and coach-only cash payment permission.

`+ `
${btn('Add parent',`editParent()`,true)}${btn('Test parent sign-up',`go('Parent Registration')`)}
`+ cc120GuardianParents().map(p=>`
${ccPersonV24('parents',p.id,p.name)}${esc(p.email||'No email')} · ${esc(p.phone||'No phone')} · Cash: ${p.cashApproved?'Allowed':'Not allowed'}
${btn('Open full profile',`ccViewProfileV25('parents','${p.id}')`,true)}${btn('Edit details / cash',`editParent('${p.id}')`)}
`).join('')+ (cc120GuardianParents().length?'':'

No parent accounts yet.

')); }; /* V35: registration review and approvals in the LOCAL DEMO. This does not create Supabase accounts or send notifications across devices. */ function ccPendingParentsV35(){return (db.parents||[]).filter(p=>p.status==='Pending').length+(db.registrations||[]).filter(r=>String(r.status||'').startsWith('Pending')).length} function ccReviewRegistrationV35(rid,approve){ if(role!=='admin')return; const r=(db.registrations||[]).find(x=>x.id===rid); if(!r||!String(r.status||'').startsWith('Pending'))return; if(!approve){r.status='Declined';notice('Parent registration declined (demo): '+r.name);save();render();return} let p=db.parents.find(p=>String(p.email||'').trim().toLowerCase()===String(r.email||'').trim().toLowerCase()); if(!p){p={id:id(),name:r.name,email:r.email,phone:r.phone||'',facility:r.facility||'',status:'Active',cashApproved:false};db.parents.push(p)} else {p.status='Active';p.name=r.name||p.name;p.phone=r.phone||p.phone;p.facility=r.facility||p.facility} r.status='Approved';r.parentId=p.id; if(r.athlete&&!(db.athleteRequestsV20||[]).some(x=>x.parentId===p.id&&x.name===r.athlete)){ db.athleteRequestsV20 ||= []; db.athleteRequestsV20.unshift({id:id(),parentId:p.id,name:r.athlete,dob:r.dob||'',goal:r.goal||'',status:'Pending approval'}); } notice('Parent registration approved (demo): '+p.name);save();render(); alert('Parent approved in this browser demonstration. The athlete profile, if provided, is still awaiting separate approval. No Supabase account was created.'); } function ccDeclineDemoParentV35(pid){ if(role!=='admin')return;const p=db.parents.find(x=>x.id===pid); if(!p||p.status!=='Pending')return;p.status='Declined';notice('Parent account request declined (demo): '+p.name);save();render(); } function ccParentApprovalsV35(){ const pending=(db.parents||[]).filter(p=>p.status==='Pending'); const registrations=(db.registrations||[]).filter(r=>String(r.status||'').startsWith('Pending')); return panel('Parent account requests · '+(pending.length+registrations.length)+' pending', `

Requests submitted through the local registration form appear here. Review and approve or decline each one. Demo-only: requests on other devices will not appear until Supabase registration is connected.

`+ table(['Request','Parent','Athlete','Facility','Status','Decision'],[ ...pending.map(p=>row(['Parent sign-up',esc(p.name), '—',esc(p.facility||'—'),esc(p.status),btn('Approve',`approveDemoParentV20('${p.id}')`,true)+' '+btn('Decline',`ccDeclineDemoParentV35('${p.id}')`)])), ...registrations.map(r=>row(['Registration form',esc(r.name),esc(r.athlete||'—'),esc(r.facility||'—'),esc(r.status),btn('Approve',`ccReviewRegistrationV35('${r.id}',true)`,true)+' '+btn('Decline',`ccReviewRegistrationV35('${r.id}',false)`)])) ])); } menus.admin['People'] ||= []; if(!menus.admin['People'].includes('Parent Account Approvals'))menus.admin['People'].splice(1,0,'Parent Account Approvals'); views['Parent Account Approvals']=()=>role==='admin'?ccParentApprovalsV35():panel('Restricted','Administrator access only.'); const ccParentsBeforeV35=views['Parents & Guardians']; views['Parents & Guardians']=()=>role==='admin'?panel('Parent approval queue',`${ccPendingParentsV35()} pending parent requests
${btn('Review parent requests',"go('Parent Account Approvals')",true)}
`)+ccParentsBeforeV35():ccParentsBeforeV35(); const ccDashboardBeforeV35=views['Dashboard']; views['Dashboard']=()=>{ if(role!=='admin')return ccDashboardBeforeV35(); const count=ccPendingParentsV35(); return panel('Parent account requests · '+count+' pending',`
${btn('Review parent approvals',"go('Parent Account Approvals')",true)}
${count?'

New parent registrations are waiting for your approval.

':'

No pending parent account requests.

'}`)+ccDashboardBeforeV35(); }; // Preserve prior request records; only a new form submission changes the queue. const ccSaveRegistrationBeforeV35=saveRegistration; saveRegistration=function(f){ccSaveRegistrationBeforeV35(f);}; /* V36: concise, activity-driven dashboards. Demo data stays browser-local. */ function ccTodayV36(){const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`} function ccActiveLessonV36(l){return l&&l.date>=ccTodayV36()&&!['Available','Cancelled','Canceled','Deleted','Expired','Declined','Rejected'].includes(l.status)} function ccLessonRowsV36(ls,context){return table(['When','Athlete','Facility','Status'],ls.slice(0,6).map(l=>row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility||'—'),esc(l.status||'—')])))} function ccUpcomingV36(ls,title){const arr=ls.filter(ccActiveLessonV36).sort((a,b)=>String(a.date+' '+a.time).localeCompare(String(b.date+' '+b.time)));return arr.length?panel(title+' · '+arr.length,ccLessonRowsV36(arr)):''} function ccIdentityV36(){ if(role==='admin')return panel('Coach account',ccProfileCardV24('admin','coach','Champions Core Coach',false)); if(role==='parent'){const p=db.parents.find(p=>p.id===parentId);return panel('My profile',ccProfileCardV24('parents',parentId,p?.name||'Parent / Guardian',false))} const a=db.athletes.find(a=>a.id===athleteId);return panel('My profile',ccProfileCardV24('athletes',athleteId,a?.name||'Athlete',false)); } function ccAdminActivityV36(){ let sections=''; const parents=ccPendingParentsV35(); if(parents)sections+=panel('Parent account requests · '+parents+' pending',`

New parent registrations need your review.

${btn('Review parent approvals',"go('Parent Account Approvals')",true)}
`); const profiles=(db.athleteRequestsV20||[]).filter(r=>r.status==='Pending approval'); const accounts=(db.athleteAccessV19||[]).filter(r=>r.status==='Pending approval'); const athletes=profiles.length+accounts.length; if(athletes)sections+=panel('Athlete account requests · '+athletes+' pending',`

New athlete profiles or athlete logins are waiting for approval.

${btn('Review athlete approvals',"go('Athlete Account Approvals')",true)}
`); const pendingLessons=(db.requests||[]).filter(r=>r.status==='Pending approval'); if(pendingLessons.length)sections+=panel('Lesson requests · '+pendingLessons.length+' pending',`
${btn('Review lesson requests',"go('New Lesson Requests')",true)}
`); const payments=(db.payments||[]).filter(p=>p.status==='Awaiting verification'&&!p.voided); if(payments.length)sections+=panel('Payments awaiting confirmation · '+payments.length,`
${btn('Verify payments',"go('Financial Overview')",true)}
`); const lessons=(db.lessons||[]).filter(ccActiveLessonV36); const today=lessons.filter(l=>l.date===ccTodayV36()); if(today.length)sections+=panel("Today's lessons · "+today.length,ccLessonRowsV36(today.sort((a,b)=>String(a.time).localeCompare(String(b.time))))); const later=lessons.filter(l=>l.date>ccTodayV36()); sections+=ccUpcomingV36(later,'Upcoming lessons'); return sections||panel('All caught up',`

No pending approvals, payments to verify, or upcoming lessons right now.

`); } function ccParentActivityV36(){ let sections='';const p=db.parents.find(x=>x.id===parentId); if(p?.status==='Pending')sections+=panel('Account awaiting approval','

Your registration is awaiting coach review.

'); const registrations=(db.registrations||[]).filter(r=>String(r.email||'').toLowerCase()===String(p?.email||'').toLowerCase()&&String(r.status||'').startsWith('Pending')); if(registrations.length)sections+=panel('Registration under review',`

${registrations.length} registration request(s) awaiting coach approval.

`); const athleteRequests=(db.athleteRequestsV20||[]).filter(r=>r.parentId===parentId&&r.status==='Pending approval'); const loginRequests=(db.athleteAccessV19||[]).filter(r=>r.parentId===parentId&&r.status==='Pending approval'); if(athleteRequests.length+loginRequests.length)sections+=panel('Athlete access awaiting approval',`

${athleteRequests.length+loginRequests.length} athlete profile or login request(s) awaiting coach review.

`); const payments=(db.payments||[]).filter(x=>x.parentId===parentId&&!x.voided&&['Awaiting payment','Awaiting verification'].includes(x.status)); if(payments.length)sections+=panel('Payments needing attention · '+payments.length,`
${btn('View payments',"go('My Payments')",true)}
`); sections+=ccUpcomingV36((db.lessons||[]).filter(l=>l.parentId===parentId),'Upcoming family lessons'); return sections||panel('All caught up',`

No upcoming lessons or requests needing attention.

${btn('Book a lesson',"go('Book a Lesson')",true)}
`); } function ccAthleteActivityV36(){ let sections='';const selected=window.athletePreviewSelectionV22||athleteId; if(String(selected).startsWith('request:')){ const r=(db.athleteRequestsV20||[]).find(x=>'request:'+x.id===selected); return panel('Profile request status',`

${esc(r?.name||'Athlete')}: ${esc(r?.status||'Pending review')}

`); } const a=db.athletes.find(x=>x.id===athleteId); if(!a)return panel('Athlete profile','No athlete profile selected.'); if(!a.athletePortalApproved){const req=(db.athleteAccessV19||[]).find(r=>r.athleteId===a.id);sections+=panel('Athlete account status',`

${esc(req?.status||'Awaiting coach approval')}

`)} sections+=ccUpcomingV36((db.lessons||[]).filter(l=>l.athleteId===athleteId),'My upcoming lessons'); const reports=(db.reports||[]).filter(r=>r.athleteId===athleteId&&String(r.status).trim()==='Published to family'); if(reports.length)sections+=panel('Published lesson reports · '+reports.length,`
${btn('View my lesson reports',"go('My Lesson Reports')",true)}
`); return sections||panel('All caught up','

No upcoming lessons or new items needing attention.

'); } views['Dashboard']=()=>ccIdentityV36()+(role==='admin'?ccAdminActivityV36():role==='parent'?ccParentActivityV36():ccAthleteActivityV36()); /* V37: strictly activity-only dashboard, regardless of legacy dashboard wrappers. */ function ccDashboardV37(){ const profile=ccIdentityV36(); const activity=role==='admin'?ccAdminActivityV36():role==='parent'?ccParentActivityV36():ccAthleteActivityV36(); return `
${profile}${activity}
`; } views['Dashboard']=ccDashboardV37; /* Last-resort cleanup if a future legacy panel is accidentally added: hide request panels whose title explicitly reports zero pending. */ function ccHideEmptyDashboardV37(){ if(page!=='Dashboard')return; const app=document.getElementById('app');if(!app)return; const panels=app.querySelectorAll('.panel'); for(const p of panels){ const heading=p.querySelector('h2,h3,h4');if(!heading)continue; const title=heading.textContent.trim(); if(/(?:parent|athlete) account requests?\s*[·:—-]\s*0\s*pending/i.test(title))p.remove(); } } const ccRenderBeforeV37=render; render=function(){ ccRenderBeforeV37(); if(page==='Dashboard'){ ccHideEmptyDashboardV37(); const title=document.getElementById('title'); if(title)title.title='Dynamic dashboard v37'; } }; render(); /* V38 — Admin-only permanent deletion of an unlinked LOCAL DEMO parent profile. Do not silently destroy athletes, lesson histories, or financial records. */ function ccDeleteParentV38(pid){ if(role!=='admin')return alert('Only the administrator can delete a parent profile.'); const p=db.parents.find(x=>x.id===pid);if(!p)return alert('This parent profile no longer exists.'); const linkedAthletes=(db.athletes||[]).filter(x=>x.parentId===pid); const linkedLessons=(db.lessons||[]).filter(x=>x.parentId===pid||linkedAthletes.some(a=>a.id===x.athleteId)); const linkedPayments=(db.payments||[]).filter(x=>x.parentId===pid||linkedLessons.some(l=>l.id===x.lessonId)); const linkedCredits=(db.credits||[]).filter(x=>x.parentId===pid); const linkedRequests=(db.requests||[]).filter(x=>x.parentId===pid); const blockers=[]; if(linkedAthletes.length)blockers.push(`${linkedAthletes.length} linked athlete(s)`); if(linkedLessons.length)blockers.push(`${linkedLessons.length} lesson(s)`); if(linkedPayments.length)blockers.push(`${linkedPayments.length} payment record(s)`); if(linkedCredits.length)blockers.push(`${linkedCredits.length} credit record(s)`); if(linkedRequests.length)blockers.push(`${linkedRequests.length} lesson request(s)`); if(blockers.length){ alert('Cannot permanently delete '+p.name+' while related records exist: '+blockers.join(', ')+'.\n\nReassign or resolve related records first to protect athlete and financial history. Archiving the parent is an alternative.'); return; } if(!confirm(`PERMANENTLY DELETE ${p.name}?\n\nThis removes the parent profile from this browser demo, including its profile photo, invitations and registration records. This cannot be undone.\n\nThis does NOT delete a Supabase Auth account.`))return; const typed=prompt(`Type DELETE to confirm permanent removal of ${p.name}:`); if(typed!=='DELETE')return alert('Deletion cancelled.'); db.parents=db.parents.filter(x=>x.id!==pid); if(db.profilePhotosV24?.parents)delete db.profilePhotosV24.parents[pid]; if(Array.isArray(db.registrations))db.registrations=db.registrations.filter(x=>x.parentId!==pid&&String(x.email||'').toLowerCase()!==String(p.email||'').toLowerCase()); if(Array.isArray(db.invitations))db.invitations=db.invitations.filter(x=>String(x.email||'').toLowerCase()!==String(p.email||'').toLowerCase()); if(Array.isArray(db.familyNoticesV18))db.familyNoticesV18=db.familyNoticesV18.filter(x=>x.parentId!==pid); if(Array.isArray(db.athleteAccessV19))db.athleteAccessV19=db.athleteAccessV19.filter(x=>x.parentId!==pid); if(Array.isArray(db.messages))db.messages=db.messages.filter(x=>x.parentId!==pid); if(Array.isArray(db.athleteRequestsV20))db.athleteRequestsV20=db.athleteRequestsV20.filter(x=>x.parentId!==pid); if(parentId===pid)parentId=db.parents[0]?.id||''; ccOpenProfileV25=null; save();closeModal();go('Parents & Guardians'); alert('Parent profile permanently removed from this browser demo.'); } const ccEditParentBeforeV38=editParent; editParent=function(pid=''){ ccEditParentBeforeV38(pid); if(role!=='admin'||!pid)return; const f=document.querySelector('#modalroot form[data-id]'); if(!f)return; const actions=f.querySelector('.actions'); if(!actions)return; const button=document.createElement('button');button.type='button';button.textContent='Delete parent profile'; button.style.cssText='background:#752d34;color:#fff;border:1px solid #c26972;margin-left:auto'; button.onclick=()=>ccDeleteParentV38(pid); actions.append(button); }; const ccProfileViewBeforeV38=views['Account Profile']; views['Account Profile']=function(){ const html=ccProfileViewBeforeV38(); if(role!=='admin'||ccOpenProfileV25?.type!=='parents')return html; const pid=ccOpenProfileV25.pid; if(!db.parents.some(p=>p.id===pid))return html; return html+panel('Danger zone',`

Permanently remove this parent profile from the local demonstration. Profiles with linked athletes, lessons, or financial records cannot be deleted until those records are resolved.

${btn('Delete parent profile',`ccDeleteParentV38('${pid}')`)}
`); }; const ccParentsBeforeV38=views['Parents & Guardians']; views['Parents & Guardians']=function(){ const html=ccParentsBeforeV38(); if(role!=='admin')return html; // Delete action in the directory as well as in the edit modal and full profile. return html.replace(/(]*onclick="editParent\('([^']+)'\)"[^>]*>Edit details \/ cash<\/button>)/g, (whole,button,pid)=>button+btn('Delete',`ccDeleteParentV38('${pid}')`)); }; /* V39 — local-demo athlete removal, media removal and coach-controlled messaging access. */ function ccDeleteAthleteV39(aid){ if(role!=='admin')return alert('Only the coach can delete athlete profiles.'); const a=db.athletes.find(x=>x.id===aid);if(!a)return alert('Athlete not found.'); const lessons=(db.lessons||[]).filter(x=>x.athleteId===aid); const payments=(db.payments||[]).filter(x=>lessons.some(l=>l.id===x.lessonId)); const requests=(db.requests||[]).filter(x=>x.athleteId===aid); const credits=(db.credits||[]).filter(x=>x.athleteId===aid); const blockers=[]; if(lessons.length)blockers.push(lessons.length+' lesson(s)'); if(payments.length)blockers.push(payments.length+' payment(s)'); if(requests.length)blockers.push(requests.length+' booking request(s)'); if(credits.length)blockers.push(credits.length+' credit(s)'); if(blockers.length)return alert('Cannot delete '+a.name+' while linked booking or financial records exist: '+blockers.join(', ')+'. Resolve these records first or archive the athlete.'); if(!confirm('Permanently delete '+a.name+' from this browser demo? This also removes local goals, skills, reports, achievements and media linked to this athlete. This cannot be undone. Supabase data will not be deleted.'))return; if(prompt('Type DELETE to permanently remove '+a.name+':')!=='DELETE')return alert('Deletion cancelled.'); db.athletes=db.athletes.filter(x=>x.id!==aid); for(const key of ['goals','skills','achievements','awards','reports','mediaItems','athleteAccessV19','athleteRequestsV20']){ if(Array.isArray(db[key]))db[key]=db[key].filter(x=>x.athleteId!==aid && x.id!==aid); } if(db.profilePhotosV24?.athletes)delete db.profilePhotosV24.athletes[aid]; if(db.profilePhotos?.athletes)delete db.profilePhotos.athletes[aid]; if(athleteId===aid)athleteId=db.athletes[0]?.id||''; ccOpenProfileV25=null;save();closeModal();go('Athletes');alert('Athlete removed from this browser demo.'); } const ccEditAthleteBeforeV39=editAthlete; editAthlete=function(aid=''){ ccEditAthleteBeforeV39(aid); if(role!=='admin'||!aid)return; const formEl=document.querySelector('#modalroot form[data-id]');if(!formEl)return; const actions=formEl.querySelector('.actions'); if(actions){const b=document.createElement('button');b.type='button';b.textContent='Delete athlete profile';b.style.cssText='background:#752d34;color:#fff;border:1px solid #c26972;margin-left:auto';b.onclick=()=>ccDeleteAthleteV39(aid);actions.append(b);} const a=db.athletes.find(x=>x.id===aid); if(a){const block=document.createElement('div');block.className='wide notice';block.innerHTML='Athlete messaging permission

Coach controls whether this athlete may contact the coach through portal messaging when two-way messaging is enabled.

'; const grid=formEl.querySelector('.formgrid');if(grid)grid.append(block);else formEl.insertBefore(block,actions||null);} }; const ccSaveAthleteBeforeV39=saveAthlete; saveAthlete=function(f){const aid=f.dataset.id;const allowed=!!f.elements.ccAthleteMessageAllowedV39?.checked;ccSaveAthleteBeforeV39(f);if(aid){const a=db.athletes.find(x=>x.id===aid);if(a){a.messageAllowedV39=allowed;save();}}}; const ccAthleteDirectoryBeforeV39=views['Athletes']; views['Athletes']=function(){const html=ccAthleteDirectoryBeforeV39();if(role!=='admin')return html;return html.replace(/(]*onclick="editAthlete\('([^']+)'\)"[^>]*>Edit details<\/button>)/g,(whole,button,aid)=>button+btn('Delete',`ccDeleteAthleteV39('${aid}')`));}; const ccAccountProfileBeforeV39=views['Account Profile']; views['Account Profile']=function(){const html=ccAccountProfileBeforeV39();if(role!=='admin'||ccOpenProfileV25?.type!=='athletes')return html;const aid=ccOpenProfileV25.pid;if(!db.athletes.some(a=>a.id===aid))return html;return html+panel('Athlete account controls',`
${btn('Edit athlete details',`editAthlete('${aid}')`)}${btn('Delete athlete profile',`ccDeleteAthleteV39('${aid}')`)}
`);}; function ccDeleteMediaV39(mid){ if(role!=='admin')return alert('Only the coach can delete media.'); const item=(db.mediaItems||[]).find(m=>m.id===mid);if(!item)return alert('Media not found.'); if(!confirm('Delete media entry "'+(item.title||'Untitled')+'"? This removes it from all local demo views.'))return; db.mediaItems=db.mediaItems.filter(m=>m.id!==mid);save();render(); } function ccMediaV39(){ const items=(db.mediaItems||[]).filter(m=>role==='admin'||(role==='parent'&&m.parent&&db.athletes.some(a=>a.id===m.athleteId&&a.parentId===parentId))||(role==='athlete'&&m.athlete&&m.athleteId===athleteId)); const head=role==='admin'?`${btn('Upload media — local preview','uploadMediaV7()',true)}
Demo metadata only; test video files are not persisted. Do not upload actual minors’ media here.
`:'

Media shared with your account.

'; return panel('Coaching media library',head+(items.length?items.map(m=>`
${esc(m.title||'Untitled')} · ${esc(aname(m.athleteId))}

${esc(m.notes||'')}

Visible to: ${['coach',m.parent?'parent':null,m.athlete?'athlete':null].filter(Boolean).join(', ')}

${m.demoVideo?``:''}${role==='admin'?`
${btn('Delete media',`ccDeleteMediaV39('${m.id}')`)}
`:''}
`).join(''):'
No shared media yet.
')); } views['Media Library']=ccMediaV39;views['My Media']=ccMediaV39; function ccToggleParentMessagesV39(pid){ if(role!=='admin')return; const p=db.parents.find(x=>x.id===pid);if(!p)return; const next=p.messageAllowedV39===false; if(!confirm((next?'Allow':'Disable')+' parent-to-coach messaging for '+p.name+'?'))return; p.messageAllowedV39=next;save();render(); } function ccToggleAthleteMessagesV39(aid){ if(role!=='admin')return; const a=db.athletes.find(x=>x.id===aid);if(!a)return; const next=!a.messageAllowedV39; if(!confirm((next?'Allow':'Disable')+' athlete-to-coach messaging for '+a.name+'?'))return; a.messageAllowedV39=next;save();render(); } const ccParentProfileBeforeV39=views['Account Profile']; views['Account Profile']=function(){const html=ccParentProfileBeforeV39();if(role!=='admin'||ccOpenProfileV25?.type!=='parents')return html;const pid=ccOpenProfileV25.pid;const p=db.parents.find(x=>x.id===pid);if(!p)return html;return html+panel('Messaging privileges',`

${p.messageAllowedV39===false?'Parent-to-coach messaging disabled':'Parent-to-coach messaging allowed'}.

Coach announcements and booking notifications remain visible. This is a local demonstration setting.

${btn(p.messageAllowedV39===false?'Enable parent messaging':'Disable parent messaging',`ccToggleParentMessagesV39('${pid}')`)}`);}; const ccParentEditBeforeV39=editParent; editParent=function(pid=''){ ccParentEditBeforeV39(pid); if(role!=='admin'||!pid)return; const f=document.querySelector('#modalroot form[data-id]');if(!f)return; const p=db.parents.find(x=>x.id===pid);if(!p)return; const block=document.createElement('div');block.className='wide notice';block.innerHTML='Parent messaging privilege

Only the coach can change this setting. Turning it off prevents this parent from sending new portal messages when two-way messaging is available.

'; const grid=f.querySelector('.formgrid');if(grid)grid.append(block);else f.insertBefore(block,f.querySelector('.actions')); }; const ccParentSaveBeforeV39=saveParent; saveParent=function(f){const pid=f.dataset.id;const allowed=!!f.elements.ccParentMessageAllowedV39?.checked;ccParentSaveBeforeV39(f);if(pid){const p=db.parents.find(x=>x.id===pid);if(p){p.messageAllowedV39=allowed;save();}}}; const ccMessagesBeforeV39=views['Messages']; views['Messages']=function(){const original=ccMessagesBeforeV39();if(role==='parent'&&db.parents.find(p=>p.id===parentId)?.messageAllowedV39===false)return panel('Messaging access','

Your ability to send messages to the coach has been disabled. You can still read coach announcements and important lesson notifications.

')+original; if(role==='athlete'&&!db.athletes.find(a=>a.id===athleteId)?.messageAllowedV39)return panel('Messaging access','

Direct athlete messaging is not enabled for this account.

')+original; return original;}; /* Preserve any existing message-send pathway with a permission check. */ const ccSaveMessageBeforeV39=saveMessage; saveMessage=function(f){if(role==='parent'&&db.parents.find(p=>p.id===parentId)?.messageAllowedV39===false)return alert('Messaging has been disabled for this account.');if(role==='athlete'&&!db.athletes.find(a=>a.id===athleteId)?.messageAllowedV39)return alert('Athlete messaging is not enabled.');return ccSaveMessageBeforeV39(f);}; /* V40: Quietly hide restricted messaging controls. Keep incoming coach communications. */ function ccCanSendV40(){ if(role==='parent')return db.parents.find(p=>p.id===parentId)?.messageAllowedV39!==false; if(role==='athlete')return db.athletes.find(a=>a.id===athleteId)?.messageAllowedV39===true; return role==='admin'; } const ccMessagesBeforeV40=views['Messages']; views['Messages']=function(){ if(role==='parent'){ const notices=(db.familyNoticesV18||[]).filter(n=>n.parentId===parentId||n.parentId==='ALL'); const direct=(db.messages||[]).filter(m=>m.parentId===parentId); const entries=[...notices.map(n=>({date:n.date,title:'Champions Core Coach',body:n.message})),...direct.map(m=>({date:m.date||'',title:m.subject||'Message from your coach',body:m.body}))]; const incoming=panel('Messages from Champions Core',entries.length?entries.map(n=>`
${ccAvatarV24('admin','coach','Coach')}
${esc(n.title)}${n.date?`

${esc(new Date(n.date).toLocaleString())}

`:''}

${esc(n.body||'')}

`).join(''):'

No messages yet.

'); // No status banner, no sender-photo explanation, and no compose control when disabled. if(!ccCanSendV40())return incoming; return incoming+panel('Send a message',btn('Compose message','compose()',true)); } if(role==='athlete'&&!ccCanSendV40()){ const notices=(db.familyNoticesV18||[]).filter(n=>n.parentId===db.athletes.find(a=>a.id===athleteId)?.parentId||n.parentId==='ALL'); return panel('Messages from Champions Core',notices.length?notices.map(n=>`

${esc(n.message||'')}

`).join(''):'

No messages yet.

'); } return ccMessagesBeforeV40(); }; const ccComposeBeforeV40=compose; compose=function(){if(!ccCanSendV40())return;return ccComposeBeforeV40();}; const ccSaveBeforeV40=saveMessage; saveMessage=function(f){if(!ccCanSendV40()){closeModal();return;}return ccSaveBeforeV40(f);}; /* Version 42: role-isolated, account-specific notification preference UI (local demo). */ const ccNotifyCatalogV42={ admin:[['parent_request','Parent account requests','A new parent registration needs approval'],['athlete_request','Athlete account requests','A new athlete account needs approval'],['parent_message','Parent messages','A parent sends you a message'],['payment_verify','Payment verification','A payment needs your confirmation'],['new_booking','New lesson bookings','A new booking or scheduling request arrives'],['schedule_change','Cancellations & reschedules','A booking changes or needs your attention']], parent:[['parent_approved','Parent account approved','Your registration is approved'],['athlete_approved','Athlete account approved','An athlete login or profile is approved'],['payment_approved','Payment approved','The coach confirms a payment'],['booking_approved','Booking approved','A requested lesson is confirmed'],['cancellation','Lesson cancellations','A lesson is cancelled'],['reschedule','Lesson reschedules','A lesson time is changed or approved']], athlete:[['skill_awarded','New skills','The coach verifies a skill'],['lesson_booked','Lessons booked for me','A lesson is scheduled for you'],['coach_message','Coach messages','A message is addressed to you'],['coach_note','Coach notes & lesson reports','New coaching feedback is published'],['achievement','Achievements & certificates','You earn an achievement or certificate']] }; function ccNotifyIdentityV42(){return role==='admin'?'admin':role==='parent'?parentId:athleteId;} function ccNotifyStoreV42(){db.notificationPreferencesV42 ||= {admin:{},parent:{},athlete:{}};return db.notificationPreferencesV42;} function ccNotifyPrefsV42(){const store=ccNotifyStoreV42(),key=ccNotifyIdentityV42();store[role] ||= {};if(!store[role][key]){store[role][key]={channels:{inapp:true,push:true,email:true},events:Object.fromEntries(ccNotifyCatalogV42[role].map(x=>[x[0],true]))};save();}return store[role][key];} function ccSetNotifyV42(group,key,value){const p=ccNotifyPrefsV42();if(group==='channels')p.channels[key]=value;else p.events[key]=value;save();render();} function ccNotifySettingsV42(){const p=ccNotifyPrefsV42(),catalog=ccNotifyCatalogV42[role];const channelRows=[['inapp','In-portal notifications','Show alerts inside Champions Core'],['push','Phone push notifications','Allow phone alerts when push service is connected'],['email','Email notifications','Send email alerts when delivery is connected']];const checkbox=(group,key,on)=>``;const rows=arr=>arr.map(([key,name,desc])=>`
${esc(name)}

${esc(desc)}

${checkbox(arr===channelRows?'channels':'events',key,arr===channelRows?p.channels[key]:p.events[key]!==false)}
`).join('');return panel('My notification preferences',`

These preferences belong only to your ${role} account. Other account roles have different notification choices.

`)+panel('How I receive alerts',rows(channelRows))+panel('Notify me about',rows(catalog))+panel('Delivery status','

Preferences are saved in this browser for demonstration. Real phone push, email delivery, cross-device synchronization, and automatic event delivery will require Supabase integration and a notification service. These switches do not yet send alerts.

');} views['Notification Settings']=ccNotifySettingsV42; menus.admin.Administration ||= [];if(!menus.admin.Administration.includes('Notification Settings'))menus.admin.Administration.push('Notification Settings'); menus.parent['Account & Family'] ||= [];if(!menus.parent['Account & Family'].includes('Notification Settings'))menus.parent['Account & Family'].push('Notification Settings'); menus.athlete['My Account'] ||= [];if(!menus.athlete['My Account'].includes('Notification Settings'))menus.athlete['My Account'].push('Notification Settings'); /* V43: personal lesson reminder choices. Local demo settings, no delivery until backend wired. */ function ccReminderV43(){ const p=ccNotifyPrefsV42(); if(!p.remindersV43)p.remindersV43={enabled:true,email:true,sms:false,times:['24h','2h'],phone:'',emailAddress:''}; return p.remindersV43; } function ccReminderUpdateV43(field,value){ if(role!=='admin'&&role!=='parent')return; const r=ccReminderV43();r[field]=value;save();render(); } function ccReminderTimeV43(key,checked){ if(role!=='admin'&&role!=='parent')return; const r=ccReminderV43();r.times=checked?[...new Set([...r.times,key])]:r.times.filter(x=>x!==key);save();render(); } function ccReminderPanelV43(){ if(role!=='admin'&&role!=='parent')return ''; const r=ccReminderV43(); const cb=(checked,action)=>``; const row=(label,desc,control)=>`
${label}

${desc}

${control}
`; const choices=[['7d','7 days before'],['48h','48 hours before'],['24h','24 hours before'],['12h','12 hours before'],['2h','2 hours before'],['1h','1 hour before']]; return panel('Lesson reminder preferences',`

Choose when to receive reminders for your ${role==='admin'?'coaching schedule':'family’s scheduled lessons'}. These settings are private to your account.

`+ row('Lesson reminders','Turn scheduled lesson reminders on or off.',cb(r.enabled,"ccReminderUpdateV43('enabled',this.checked)"))+ `

Remind me

`+choices.map(([key,label])=>row(label,'Before the lesson begins',cb(r.times.includes(key),`ccReminderTimeV43('${key}',this.checked)`))).join('')+ `

Send reminders by

`+row('Email','Receive lesson reminders by email.',cb(r.email,"ccReminderUpdateV43('email',this.checked)"))+ row('Text message (SMS)','Receive lesson reminders by text.',cb(r.sms,"ccReminderUpdateV43('sms',this.checked)"))+ `
`+ `

Delivery not yet active: Your choices are saved in this browser only. Email/SMS delivery, verified contact details, opt-in/opt-out, time-zone-aware scheduling, reschedule/cancellation handling, and signed agreements still require the secure backend and messaging provider. No messages are sent by these controls yet.

`); } const ccNotifyBeforeV43=views['Notification Settings']; views['Notification Settings']=function(){return ccNotifyBeforeV43()+ccReminderPanelV43();}; const ccSettingsBeforeV43=views['Settings']; if(typeof ccSettingsBeforeV43==='function')views['Settings']=function(){return ccSettingsBeforeV43()+ccReminderPanelV43();}; // Replace the outdated fixed-defaults informational box in admin Settings with an actionable shortcut. const ccSettingsV7BeforeV43=settingsV7; settingsV7=function(){return ccSettingsV7BeforeV43().replace(/

Reminder defaults: 24 hours and 2 hours before a lesson\. Email\/SMS, signed agreements, account approval enforcement and backups require a connected backend\.<\/p>/,`

Set your own lesson reminder times and choose email, SMS, or both. Each parent can set their own preferences separately.

${btn('Manage my reminders',"go('Notification Settings')",true)}

Email/SMS delivery, signed agreements, approval enforcement, and backups still need secure backend integration before launch.

`);}; /* V44: Athlete-specific optional email and SMS contact details, stored per athlete preview account. */ function ccAthleteContactV44(){ if(role!=='athlete')return null; const p=ccNotifyPrefsV42(); if(!p.athleteContactV44)p.athleteContactV44={email:'',phone:'',emailEnabled:false,smsEnabled:false}; return p.athleteContactV44; } function ccAthleteContactUpdateV44(field,value){ if(role!=='athlete')return; const c=ccAthleteContactV44(); if(field==='email')value=String(value).trim(); if(field==='phone')value=String(value).trim(); c[field]=value;save();render(); } function ccAthleteContactPanelV44(){ if(role!=='athlete')return ''; const c=ccAthleteContactV44(); const check=(field,label,desc)=>`
${label}

${desc}

`; return panel('My notification contact details',`

Add an email address or phone number if you have one. Both are optional. These contact details and delivery choices belong only to your athlete account.

`+ check('emailEnabled','Email alerts','Send my selected athlete notifications to my email, when connected.')+ check('smsEnabled','Text message (SMS) alerts','Send my selected athlete notifications to my mobile number, when connected.')+ `

Email and SMS are optional. For athletes under 18, guardian authorization and verified contact/consent will be required before live messaging is enabled. These settings currently save only in this browser and do not send messages.

`); } const ccNotifyBeforeV44=views['Notification Settings']; views['Notification Settings']=function(){return ccNotifyBeforeV44()+ccAthleteContactPanelV44();}; /* V46 — meaningful parent/athlete profiles and contextual pending requests. Local demo only. */ function ccParentEditDetailsV46(type,pid){ if(role!=='parent')return; const person=type==='athletes'?db.athletes.find(a=>a.id===pid&&a.parentId===parentId):db.parents.find(p=>p.id===pid&&p.id===parentId); if(!person)return alert('Profile not available for this account.'); const fields=type==='athletes' ?field('Athlete name','name',person.name||'')+field('Current personal goal','goal',person.goal||'') :field('Full name','name',person.name||'')+field('Email address','email',person.email||'','email')+field('Phone number','phone',person.phone||'','tel'); modal(type==='athletes'?'Edit athlete profile':'Edit my profile',form(fields,'ccSaveParentDetailsV46','Save changes').replace('
Athlete
${esc(a.name)}
Facility
${esc(a.facility||'—')}
Skill level
${esc(a.level||'—')}
Current goal
${esc(a.goal||'—')}
Status
${esc(a.status||'—')}

${btn('Edit athlete profile',`ccParentEditDetailsV46('athletes','${a.id}')`,true)}${btn('View family progress',"go('Progress & Achievements')")}${btn('View lessons',"go('My Lessons')")}
`) +panel('Lesson history',table(['Date & time','Facility','Status'],lessons.map(l=>row([fmt(l.date,l.time),esc(l.facility),esc(l.status)])))) +panel('Published lesson reports',table(['Date','Coach feedback'],reports.map(r=>row([esc(r.date),esc(r.notes||'—')])))) +panel('Skill progress',table(['Skill','Progress'],skills.map(x=>row([esc(x.name),esc(x.stage||'—')])))) +panel('Achievements & certificates',table(['Achievement','Skill'],awards.map(x=>row([esc(x.type),esc(x.skill||'—')])))); } views['My Athletes']=function(){ if(role!=='parent')return panel('My family profiles','Only parents can view this page.'); const linked=db.athletes.filter(a=>a.parentId===parentId); const pending=(db.athleteRequestsV20||[]).filter(r=>r.parentId===parentId&&r.status==='Pending approval'&&!linked.some(a=>a.name.trim().toLowerCase()===String(r.name||'').trim().toLowerCase())); return panel('Family profiles',`

Open your account or an athlete profile to review information, coaching progress and permitted profile changes.

${ccPersonV24('parents',parentId,pname(parentId))}${btn('Open my profile',`ccViewProfileV25('parents','${parentId}')`,true)}
`+linked.map(a=>`
${ccPersonV24('athletes',a.id,a.name)}${btn('Open athlete profile',`ccViewProfileV25('athletes','${a.id}')`,true)}
`).join('')+(linked.length?'':'

No athlete profiles linked yet.

')) +(pending.length?panel('Pending athlete profile requests',table(['Athlete','Status'],pending.map(r=>row([esc(r.name),esc(r.status)])))):''); }; const ccProfileBeforeV46=views['Account Profile']; views['Account Profile']=function(){ if(!ccOpenProfileV25)return panel('Account profile','Select a family profile to open it.'); const {type,pid}=ccOpenProfileV25; if(role!=='parent')return ccProfileBeforeV46(); if(type==='athletes'){ const a=db.athletes.find(x=>x.id===pid&&x.parentId===parentId); if(!a)return panel('Account profile','Access denied.'); return panel('Athlete profile',ccProfileCardV24('athletes',a.id,a.name,ccPhotoPermissionV24('athletes',a.id))+`
${btn('Back to my athletes',"go('My Athletes')")}
`)+ccAthleteOverviewV46(a); } const p=db.parents.find(x=>x.id===pid&&x.id===parentId); if(!p)return panel('Account profile','Access denied.'); return panel('My parent profile',ccProfileCardV24('parents',p.id,p.name,ccPhotoPermissionV24('parents',p.id))+`
Name
${esc(p.name||'—')}
Email
${esc(p.email||'—')}
Phone
${esc(p.phone||'—')}
Assigned facility
${esc(p.facility||a.facility||'—')} (set by coach)
Linked athletes
${esc(db.athletes.filter(a=>a.parentId===parentId).map(a=>a.name).join(', ')||'None')}
${btn('Edit my profile',`ccParentEditDetailsV46('parents','${pid}')`,true)}${btn('Back to my athletes',"go('My Athletes')")}
`); }; /* V47: family profile permissions generalized to every linked guardian/athlete pair. Local demo only. */ function ccLinkedAthletesV47(pid){ const links=db.familyLinksV47||[]; return (db.athletes||[]).filter(a=>a.parentId===pid||links.some(l=>l.parentId===pid&&l.athleteId===a.id&&l.active!==false)); } function ccCanParentViewAthleteV47(aid){return role==='parent'&&ccLinkedAthletesV47(parentId).some(a=>a.id===aid);} function ccParentEditDetailsV46(type,pid){ if(role!=='parent')return; const person=type==='athletes'?db.athletes.find(a=>a.id===pid&&ccCanParentViewAthleteV47(a.id)):db.parents.find(p=>p.id===pid&&p.id===parentId); if(!person)return alert('Profile not available for this account.'); const fields=type==='athletes'?field('Athlete name','name',person.name||'')+field('Current personal goal','goal',person.goal||''):field('Full name','name',person.name||'')+field('Email address','email',person.email||'','email')+field('Phone number','phone',person.phone||'','tel'); modal(type==='athletes'?'Edit athlete profile':'Edit my profile',form(fields,'ccSaveParentDetailsV46','Save changes').replace('
Athlete
${esc(a.name)}
Facility
${esc(a.facility||'—')}
Skill level
${esc(a.level||'—')}
Current goal
${esc(a.goal||'—')}
Status
${esc(a.status||'—')}
${btn('Edit athlete profile',`ccParentEditDetailsV46('athletes','${a.id}')`,true)}${btn('View family progress',"go('Progress & Achievements')")}${btn('View lessons',"go('My Lessons')")}
`) +panel('Lesson history',table(['Date & time','Facility','Status'],lessons.map(l=>row([fmt(l.date,l.time),esc(l.facility),esc(l.status)])))) +panel('Published lesson reports',table(['Date','Coach feedback'],reports.map(r=>row([esc(r.date),esc(r.notes||'—')])))) +panel('Skill progress',table(['Skill','Progress'],skills.map(x=>row([esc(x.name),esc(x.stage||'—')])))) +panel('Achievements & certificates',table(['Achievement','Skill'],awards.map(x=>row([esc(x.type),esc(x.skill||'—')])))); } views['My Athletes']=function(){ if(role!=='parent')return panel('My family profiles','Only parents can view this page.'); const linked=ccLinkedAthletesV47(parentId); const pending=(db.athleteRequestsV20||[]).filter(r=>r.parentId===parentId&&String(r.status||'').toLowerCase().includes('pending')&&!linked.some(a=>(r.athleteId&&r.athleteId===a.id)||a.name.trim().toLowerCase()===String(r.name||r.athleteName||'').trim().toLowerCase())); return panel('Family profiles',`

Manage your profile and open any athlete linked to your family.

${ccPersonV24('parents',parentId,pname(parentId))}${btn('Open my profile',`ccViewProfileV25('parents','${parentId}')`,true)}
`+linked.map(a=>`
${ccPersonV24('athletes',a.id,a.name)}${btn('Open athlete profile',`ccViewProfileV25('athletes','${a.id}')`,true)}
`).join('')+(linked.length?'':'

No athlete profiles linked yet.

')) +(pending.length?panel('Pending athlete profile requests',table(['Athlete','Status'],pending.map(r=>row([esc(r.name||r.athleteName||'Athlete'),esc(r.status)])))):''); }; const ccProfileBeforeV47=views['Account Profile']; views['Account Profile']=function(){ if(role!=='parent')return ccProfileBeforeV47(); if(!ccOpenProfileV25)return panel('Account profile','Select a family profile to open it.'); const {type,pid}=ccOpenProfileV25; if(type==='athletes'){ const a=db.athletes.find(x=>x.id===pid&&ccCanParentViewAthleteV47(x.id)); if(!a)return panel('Account profile','Access denied.'); return panel('Athlete profile',ccProfileCardV24('athletes',a.id,a.name,true)+`
${btn('Back to my athletes',"go('My Athletes')")}
`)+ccAthleteOverviewV46(a); } const p=db.parents.find(x=>x.id===pid&&x.id===parentId); if(!p)return panel('Account profile','Access denied.'); return panel('My parent profile',ccProfileCardV24('parents',p.id,p.name,true)+`
Name
${esc(p.name||'—')}
Email
${esc(p.email||'—')}
Phone
${esc(p.phone||'—')}
Facility
${esc(p.facility||'—')}
Linked athletes
${esc(ccLinkedAthletesV47(parentId).map(a=>a.name).join(', ')||'None')}
${btn('Edit my profile',`ccParentEditDetailsV46('parents','${pid}')`,true)}${btn('Back to my athletes',"go('My Athletes')")}
`); }; const ccPhotoPermissionBeforeV47=ccPhotoPermissionV24; ccPhotoPermissionV24=function(type,pid){ if(type==='athletes'&&role==='parent')return ccCanParentViewAthleteV47(pid); return ccPhotoPermissionBeforeV47(type,pid); }; /* V48: separate parent profile navigation and universal page back controls. */ if(menus.parent){ menus.parent['Family']=(menus.parent['Family']||[]).filter(x=>x!=='My Profile'); menus.parent['Family'].unshift('My Profile'); } views['My Profile']=function(){ if(role==='parent'){ const p=db.parents.find(x=>x.id===parentId); if(!p)return panel('My profile','Parent profile unavailable.'); ccOpenProfileV25={type:'parents',pid:p.id}; return views['Account Profile'](); } if(role==='athlete'){ const a=db.athletes.find(x=>x.id===athleteId); if(a){ccOpenProfileV25={type:'athletes',pid:a.id};return views['Account Profile']();} } return panel('My profile','Profile not available.'); }; views['My Athletes']=function(){ if(role!=='parent')return panel('My athletes','Only parent accounts can access this page.'); const linked=ccLinkedAthletesV47(parentId); const pending=(db.athleteRequestsV20||[]).filter(r=>r.parentId===parentId&&String(r.status||'').toLowerCase().includes('pending')&&!linked.some(a=>(r.athleteId&&r.athleteId===a.id)||a.name.trim().toLowerCase()===String(r.name||r.athleteName||'').trim().toLowerCase())); return panel('My athletes',`

Select an athlete to view their profile, lesson history, published progress, and editable details.

`+linked.map(a=>`
${ccPersonV24('athletes',a.id,a.name)}${btn('Open athlete profile',`ccViewProfileV25('athletes','${a.id}')`,true)}
`).join('')+(linked.length?'':'

No athletes linked to your account yet.

'))+(pending.length?panel('Pending athlete requests',table(['Athlete','Status'],pending.map(r=>row([esc(r.name||r.athleteName||'Athlete'),esc(r.status)])))):''); }; const ccProfileBeforeV48=views['Account Profile']; views['Account Profile']=function(){ let html=ccProfileBeforeV48(); if(role==='parent'&&ccOpenProfileV25?.type==='parents'){ html=html.replaceAll('Back to my athletes','Back to my profile'); // The parent profile is its own destination; avoid linking back to the athlete roster. html=html.replaceAll(`onclick="go('My Athletes')"`,`onclick="go('Dashboard')"`); } return html; }; let ccPageHistoryV48=[]; const ccGoBeforeV48=go; go=function(destination){ if(destination!==page){ccPageHistoryV48.push({page:page,profile:ccOpenProfileV25?{...ccOpenProfileV25}:null});if(ccPageHistoryV48.length>40)ccPageHistoryV48.shift();} return ccGoBeforeV48(destination); }; function ccBackV48(){ let previous=ccPageHistoryV48.pop(); if(previous){ccOpenProfileV25=previous.profile;page=previous.page;render();} else {page='Dashboard';render();} } function ccMenuV48(){ const toggle=document.querySelector('#menuToggle, #hamburger, .menu-toggle, .hamburger'); if(toggle){toggle.click();return;} const nav=document.querySelector('nav');if(nav){nav.classList.toggle('open');nav.classList.toggle('show');} } const ccRenderBeforeV48=render; render=function(){ ccRenderBeforeV48(); const app=document.getElementById('app');if(!app)return; const toolbar=document.createElement('div');toolbar.className='cc-nav-actions-v48'; toolbar.style.cssText='display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 16px'; const back=document.createElement('button');back.type='button';back.className='btn';back.textContent='← Back';back.onclick=ccBackV48; const home=document.createElement('button');home.type='button';home.className='btn';home.textContent='⌂ Dashboard';home.onclick=()=>go('Dashboard'); const menu=document.createElement('button');menu.type='button';menu.className='btn';menu.textContent='☰ Menu';menu.onclick=()=>{const nav=document.getElementById('nav');if(nav){nav.style.display=nav.style.display==='block'?'':'block';}else ccMenuV48();}; toolbar.append(back,home,menu);app.prepend(toolbar); }; const ccSetRoleBeforeV48=setRole; setRole=function(v){ccPageHistoryV48=[];ccOpenProfileV25=null;return ccSetRoleBeforeV48(v);}; render(); /* V49: one menu control in sidebar; navigation only away from Dashboard. */ const ccRenderBeforeV49=render; render=function(){ ccRenderBeforeV49(); const toolbar=document.querySelector('#app > .cc-nav-actions-v48'); if(!toolbar)return; // Dashboard is the navigation root, so neither Back nor Dashboard is needed there. if(page==='Dashboard'){toolbar.remove();return;} // The sidebar hamburger is the only menu control. toolbar.querySelectorAll('button').forEach(button=>{ if(button.textContent.includes('Menu'))button.remove(); }); }; render(); /* V50: Independent Athlete combined family/billing + athlete view. Browser-only PREVIEW. Does not create authenticated accounts or send messages. */ let ccIndependentV50=false; const ccIndependentPersonV50={pid:'p-independent-v50',aid:'a-independent-v50'}; function ccEnsureIndependentV50(){ if(!db.parents.some(p=>p.id===ccIndependentPersonV50.pid))db.parents.push({id:ccIndependentPersonV50.pid,name:'Morgan Lee',email:'adult@example.test',phone:'',facility:'World Cup East Brunswick',status:'Active'}); if(!db.athletes.some(a=>a.id===ccIndependentPersonV50.aid))db.athletes.push({id:ccIndependentPersonV50.aid,parentId:ccIndependentPersonV50.pid,name:'Morgan Lee',facility:'World Cup East Brunswick',level:'Advanced',goal:'Build consistency and confidence',status:'Active'}); save(); } const ccOriginalSetRoleV50=setRole; setRole=function(value){ ccIndependentV50=value==='independent'; if(ccIndependentV50){ccEnsureIndependentV50();parentId=ccIndependentPersonV50.pid;athleteId=ccIndependentPersonV50.aid;return ccOriginalSetRoleV50('parent');} parentId='p1';athleteId='a1';return ccOriginalSetRoleV50(value); }; const ccSelectV50=document.getElementById('role'); if(ccSelectV50&&!ccSelectV50.querySelector('option[value="independent"]')){ const option=document.createElement('option');option.value='independent';option.textContent='Individual Athlete · Preview';ccSelectV50.appendChild(option); ccSelectV50.addEventListener('change',()=>{if(ccSelectV50.value==='independent')setRole('independent');}); } const ccIndependentEventsV50=[ ['account_approved','Account approval'],['lesson_scheduled','Confirmed lesson bookings'],['appointment_request_status','Appointment request decisions'], ['lesson_cancelled','Lesson cancellations'],['lesson_rescheduled','Lesson reschedules'],['lesson_reminder','Lesson reminders'], ['payment_approved','Payment approvals'],['payment_due','Bills and payments due'],['payment_receipt','Payment receipts'], ['coach_message','Messages from coach'],['lesson_report_published','Published lesson reports'],['skill_verified','Verified skills'], ['achievement_awarded','Achievements and badges'],['certificate_issued','Champions Core Certified / certificates'],['media_published','New shared media'] ]; function ccIndependentPreferencesV50(){ db.independentPrefsV50 ||= {}; const k=ccIndependentPersonV50.pid; db.independentPrefsV50[k] ||= {events:{},channels:{email:false,sms:false}}; return db.independentPrefsV50[k]; } function ccSetIndependentPreferenceV50(group,key,checked){const p=ccIndependentPreferencesV50();p[group][key]=checked;save();render();} function ccIndependentSettingsV50(){ const p=ccIndependentPreferencesV50(); const line=(group,key,label,checked)=>`
${esc(label)}
`; return panel('Independent athlete notifications',`

Choose each event individually. Both the event and the delivery channel must be enabled. This is a local preview; no emails or texts are sent.

`) +panel('Delivery channels',line('channels','email','Email alerts',p.channels.email)+line('channels','sms','SMS text alerts',p.channels.sms)) +panel('Notify me about',ccIndependentEventsV50.map(([k,v])=>line('events',k,v,p.events[k]===true)).join('')) +panel('Before live delivery','Email/phone verification and SMS consent are required. These settings will be connected to Supabase in the production implementation.'); } const ccBeforeIndependentDashV50=views['Dashboard']; views['Dashboard']=function(){if(!ccIndependentV50)return ccBeforeIndependentDashV50();return panel('Independent Athlete · My HUB',`

One account for booking, billing, training, messaging and progress. Adult athlete preview only.

${btn('My profile',"go('My Profile')",true)}${btn('Book a lesson',"go('Book a Lesson')")}${btn('Messages',"go('Messages')")}${btn('Notifications',"go('Notification Settings')")}
`)+ccBeforeIndependentDashV50()+panel('My athlete tools',`
${btn('My skills',"go('My Skills')")}${btn('Achievements & certificates',"go('My Achievements')")}${btn('My media',"go('My Media')")}${btn('Lesson reports',"go('My Lessons')")}${btn('My bills & payments',"go('My Payments')")}
`);}; const ccBeforeIndependentSettingsV50=views['Notification Settings']; views['Notification Settings']=function(){return ccIndependentV50?ccIndependentSettingsV50():ccBeforeIndependentSettingsV50();}; const ccBeforeIndependentProfileV50=views['My Profile']; views['My Profile']=function(){return ccIndependentV50?panel('My Individual Athlete profile',`

Adult athlete account: you manage your own profile, appointments, billing and training.

${btn('Edit contact details',`ccParentEditDetailsV46('parents','${ccIndependentPersonV50.pid}')`,true)}${btn('My training profile',`ccViewProfileV25('athletes','${ccIndependentPersonV50.aid}')`)}
`)+ccBeforeIndependentProfileV50():ccBeforeIndependentProfileV50();}; const ccBeforeIndependentRenderV50=render; render=function(){ ccBeforeIndependentRenderV50(); if(!ccIndependentV50)return; const nav=document.getElementById('nav'); if(nav&&!nav.querySelector('#cc-independent-menu-v50')){ const d=document.createElement('details');d.id='cc-independent-menu-v50';d.open=true; d.innerHTML='Independent Athlete · Training'; [['My Skills','My Skills'],['My Achievements','My Achievements'],['My Media','My Media'],['My Payments','Bills & Payments'],['Notification Settings','Notification Settings']].forEach(([destination,label])=>{ const b=document.createElement('button');b.textContent=label;b.onclick=()=>go(destination);d.appendChild(b); });nav.appendChild(d); } const title=document.getElementById('title');if(title&&page==='Dashboard')title.textContent='Independent Athlete Dashboard'; }; render(); /* V52: Independent Athlete profile and navigation isolation (local preview). */ const ccV52Allowed=new Set(['Dashboard','My Profile','My Lessons','Book a Lesson','My Payments','Messages','Notification Settings','My Skills','My Achievements','My Media','Progress & Achievements','My Goals','Account Profile']); const ccV52OriginalGo=go; go=function(destination){ if(ccIndependentV50&&!ccV52Allowed.has(destination)){alert('This section is not available for your independent athlete account.');return;} if(ccIndependentV50&&destination==='Account Profile')destination='My Profile'; return ccV52OriginalGo(destination); }; const ccV52OriginalRenderNav=renderNav; renderNav=function(){ if(!ccIndependentV50)return ccV52OriginalRenderNav(); const groups=[['My HUB',[['Dashboard','Dashboard'],['My Profile','My Profile'],['Book a Lesson','Book a Lesson'],['My Lessons','My Lessons'],['My Payments','Bills & Payments'],['Messages','Messages'],['Notification Settings','Notifications']]],['My Training',[['My Goals','My Goals'],['My Skills','My Skills'],['My Achievements','Achievements & Certificates'],['My Media','My Media'],['Progress & Achievements','Progress & Achievements']]]]; $('nav').innerHTML=groups.map(([title,items])=>`
${esc(title)}${items.map(([dest,label])=>``).join('')}
`).join(''); }; const ccV52PreviousRender=render; render=function(){ if(ccIndependentV50&&!ccV52Allowed.has(page))page='Dashboard'; ccV52PreviousRender(); if(!ccIndependentV50)return; // Remove V50's duplicate training menu and legacy account switcher. document.getElementById('cc-independent-menu-v50')?.remove(); const selector=document.getElementById('role'); if(selector){selector.value='independent';const line=selector.closest('.cc-preview-accounts')||selector.parentElement;if(line){line.style.display='none';}} const title=document.getElementById('title');if(title&&page==='My Profile')title.textContent='My Profile'; }; function ccV52Person(){ccEnsureIndependentV50();return {p:db.parents.find(p=>p.id===ccIndependentPersonV50.pid),a:db.athletes.find(a=>a.id===ccIndependentPersonV50.aid)};} function ccV52EditProfile(){ if(!ccIndependentV50)return; const {p,a}=ccV52Person(); const fields=field('Full name','name',p.name||'')+field('Email address','email',p.email||'','email')+field('Phone number','phone',p.phone||'','tel')+`
Assigned facility (coach-managed)
${esc(p.facility||a.facility||'Not assigned')}
Contact your coach to request a facility change.
`+field('Skill level','level',a.level||'')+txt('My training goals','goal',a.goal||'')+txt('Additional goals and notes','personalNotes',a.personalNotes||''); modal('Edit my profile',form(fields,'ccV52SaveProfile','Save changes')); } function ccV52SaveProfile(f){ if(!ccIndependentV50)return; const {p,a}=ccV52Person();const name=val(f,'name').trim(),email=val(f,'email').trim(); if(!name||!email)return alert('Name and email are required.'); p.name=a.name=name;p.email=email;p.phone=val(f,'phone').trim();a.level=val(f,'level').trim();a.goal=val(f,'goal').trim();a.personalNotes=val(f,'personalNotes').trim(); save();closeModal();render(); } views['My Profile']=function(){ if(!ccIndependentV50)return ccBeforeIndependentProfileV50(); const {p,a}=ccV52Person(); return panel('My Profile',`

Manage your account, contact information, and personal training goals.

${ccPersonV24('parents',p.id,p.name)}
Name
${esc(p.name)}
Email
${esc(p.email||'—')}
Phone
${esc(p.phone||'—')}
Facility
${esc(p.facility||'—')}
Skill level
${esc(a.level||'—')}
Training goals
${esc(a.goal||'—')}
${btn('Edit my profile','ccV52EditProfile()',true)}${btn('My goals',"go('My Goals')")}${btn('Notification settings',"go('Notification Settings')")}
`) +panel('Additional goals & notes',`

${esc(a.personalNotes||'No additional goals recorded.')}

`); }; views['My Goals']=function(){ if(!ccIndependentV50)return panel('My goals','Your goals are available in your athlete profile.'); const {a}=ccV52Person();return panel('My Goals',`

Set and update your personal goals. Coach-verified skills and certifications are managed separately.

Training goals

${esc(a.goal||'No goals yet.')}

Additional goals & notes

${esc(a.personalNotes||'No additional goals yet.')}

${btn('Edit my goals','ccV52EditProfile()',true)}`); }; const ccV52PriorDashboard=views['Dashboard']; views['Dashboard']=function(){ if(!ccIndependentV50)return ccV52PriorDashboard(); const {p,a}=ccV52Person(); const mine=(db.lessons||[]).filter(l=>l.athleteId===a.id&&l.parentId===p.id&&l.status!=='Available'); return panel('Welcome to your HUB',`

${esc(p.name)}

Your private training, bookings, payments, progress, and messages in one place.

${btn('My Profile',"go('My Profile')",true)}${btn('Book a lesson',"go('Book a Lesson')")}${btn('Messages',"go('Messages')")}${btn('Notifications',"go('Notification Settings')")}
`) +panel('My upcoming lessons',mine.length?`

${mine.length} lesson(s) on your account.

${btn('View lessons',"go('My Lessons')",true)}`:`

No lessons booked yet.

${btn('Book a lesson',"go('Book a Lesson')",true)}`) +panel('My training',`
${btn('My Goals',"go('My Goals')")}${btn('My Skills',"go('My Skills')")}${btn('Achievements',"go('My Achievements')")}${btn('My Media',"go('My Media')")}${btn('Lesson Reports',"go('My Lessons')")}${btn('Bills & Payments',"go('My Payments')")}
`); }; // Isolate account-specific training lists from other demo athletes. const ccV52OldSkills=views['My Skills'],ccV52OldAchievements=views['My Achievements'],ccV52OldMedia=views['My Media']; views['My Skills']=function(){if(!ccIndependentV50)return ccV52OldSkills?ccV52OldSkills():panel('My Skills','');const {a}=ccV52Person();const items=(db.skills||[]).filter(s=>s.athleteId===a.id);return panel('My Skills',items.length?table(['Skill','Progress'],items.map(s=>row([esc(s.name),esc(s.stage||'—')]))):'

No verified skills posted yet.

');}; views['My Achievements']=function(){if(!ccIndependentV50)return ccV52OldAchievements?ccV52OldAchievements():panel('My Achievements','');const {a}=ccV52Person();const items=(db.awards||[]).filter(x=>x.athleteId===a.id&&x.status==='Published');return panel('My Achievements',items.length?table(['Achievement','Skill'],items.map(x=>row([esc(x.type),esc(x.skill)]))):'

No published achievements yet.

');}; views['My Media']=function(){if(!ccIndependentV50)return ccV52OldMedia?ccV52OldMedia():panel('My Media','');const {a}=ccV52Person();const items=(db.media||[]).filter(x=>x.athleteId===a.id&&x.visibility!=='Coach only'&&x.visibility!=='coach-only');return panel('My Media',items.length?`

${items.length} shared media item(s) available.

`:'

No shared media yet.

');}; render(); /* V53: Personalized dashboard headings for all preview roles. */ const ccV53PreviousRender=render; function ccV53DashboardName(){ if(role==='admin')return null; if(ccIndependentV50){ const person=ccV52Person(); return person.p?.name||person.a?.name||null; } if(role==='parent')return db.parents.find(p=>p.id===parentId)?.name||null; if(role==='athlete')return db.athletes.find(a=>a.id===athleteId)?.name||null; return null; } render=function(){ ccV53PreviousRender(); if(page!=='Dashboard')return; const heading=document.getElementById('title'); if(!heading)return; const name=ccV53DashboardName(); heading.textContent=name?`${name}'s Dashboard`:'Dashboard'; }; render(); /* V55: Clear lesson empty state, independent-account filtering, preview reschedule test. */ const ccV55PreviousLessons=views['My Lessons']; function ccV55MyLessons(){ if(role==='admin')return ccV55PreviousLessons(); let owned; if(ccIndependentV50){const {p,a}=ccV52Person();owned=db.lessons.filter(l=>l.athleteId===a.id&&l.parentId===p.id&&l.status!=='Available');} else if(role==='parent')owned=db.lessons.filter(l=>l.parentId===parentId&&l.status!=='Available'); else owned=db.lessons.filter(l=>l.athleteId===athleteId&&l.status!=='Available'); if(!owned.length){ const sample=ccIndependentV50?`
${btn('Create sample confirmed lesson (demo)', 'ccV55SampleLesson()')}

Sample data stays in this browser and is not a real booking.

`:''; return panel('My lessons — upcoming and past',`

No Lessons Scheduled

You don't have any upcoming or previous lessons.

${btn('Book a Lesson',"go('Book a Lesson')",true)}
${sample}
`); } const sorted=owned.slice().sort((a,b)=>(b.date+(b.time||'')).localeCompare(a.date+(a.time||''))); const rows=sorted.map(l=>{ const upcoming=future(l.date,l.time); const eligible=upcoming&&['Confirmed','Awaiting verification'].includes(l.status); const action=l.status==='Pending payment'&&upcoming?btn('Pay',`payForLesson('${l.id}')`,true):eligible?btn('Request reschedule',`reschedule('${l.id}')`):'—'; return row([fmt(l.date,l.time),esc(aname(l.athleteId)),esc(l.facility),esc(l.duration||30)+' min',money(l.price),esc(l.status),action]); }); return `
`+panel('My lessons — upcoming and past',`

Upcoming confirmed lessons can be rescheduled here. Past lessons remain in your history.

${btn('Export lesson history (CSV)',"exportRows('lessons')",true)}${btn('Print lesson history','printLessonHistoryV17()')}
`+table(['Date & time','Athlete','Facility','Duration','Price','Status','Action'],rows))+'
'; } views['My Lessons']=ccV55MyLessons; function ccV55SampleLesson(){ if(!ccIndependentV50)return; const {p,a}=ccV52Person(); const dt=new Date();dt.setDate(dt.getDate()+3);dt.setHours(16,0,0,0); const date=`${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`; db.lessons.push({id:id(),parentId:p.id,athleteId:a.id,facility:p.facility||a.facility||'World Cup East Brunswick',date,time:'16:00',duration:30,price:facilityPrice(p.facility||a.facility),status:'Confirmed',demoSample:true}); save();render(); } const ccV55OldReschedule=reschedule; reschedule=function(lid){ const lesson=db.lessons.find(l=>l.id===lid); if(!lesson)return alert('Lesson not found.'); if(ccIndependentV50){const {p,a}=ccV52Person();if(lesson.parentId!==p.id||lesson.athleteId!==a.id)return alert('This lesson is not yours.');} else if(role==='parent'&&lesson.parentId!==parentId)return alert('This lesson is not yours.'); else if(role==='athlete'&&lesson.athleteId!==athleteId)return alert('This lesson is not yours.'); if(!future(lesson.date,lesson.time)||!['Confirmed','Awaiting verification'].includes(lesson.status))return alert('This lesson is not eligible for rescheduling.'); ccV55OldReschedule(lid); }; render(); /* V56: Keep the DEMO role switcher available in every preview. This is a preview-only control, not access to other athletes' accounts. Independent athlete family/athlete selectors remain hidden. */ const ccV56PreviousRender = render; render = function(){ ccV56PreviousRender(); const selector = document.getElementById('role'); if (!selector) return; const wrapper = selector.closest('.tools') || selector.parentElement; if (wrapper) wrapper.style.removeProperty('display'); selector.style.removeProperty('display'); selector.value = ccIndependentV50 ? 'independent' : role; selector.setAttribute('aria-label', 'Switch fictional preview role'); selector.title = 'Demo only: switch between Admin, Parent, Athlete, and Independent Athlete previews'; }; render(); /* V57: Shared Champions Core brand header + independent athlete profile photo. Local preview only; production must use authenticated private storage. */ const ccV57PreviousPhotoPermission = ccPhotoPermissionV24; ccPhotoPermissionV24 = function(type,pid){ if(ccIndependentV50){ const mine=ccV52Person(); return type==='parents' && pid===mine.p?.id; } return ccV57PreviousPhotoPermission(type,pid); }; const ccV57PreviousPickPhoto=ccPickPhotoV24; ccPickPhotoV24=function(type,pid){ if(ccIndependentV50 && page==='My Profile' && type==='parents' && pid===ccV52Person().p?.id){ // Existing photo handler restricts entry to Account Profile; temporarily use // the same approved profile context without granting access to other users. const priorPage=page; try{page='Account Profile';return ccV57PreviousPickPhoto(type,pid);}finally{page=priorPage;} } return ccV57PreviousPickPhoto(type,pid); }; const ccV57PreviousRemovePhoto=ccRemovePhotoV24; ccRemovePhotoV24=function(type,pid){ if(ccIndependentV50 && page==='My Profile' && type==='parents' && pid===ccV52Person().p?.id){ const priorPage=page; try{page='Account Profile';return ccV57PreviousRemovePhoto(type,pid);}finally{page=priorPage;} } return ccV57PreviousRemovePhoto(type,pid); }; const ccV57PreviousMyProfile=views['My Profile']; views['My Profile']=function(){ if(!ccIndependentV50)return ccV57PreviousMyProfile(); const {p}=ccV52Person(); const photoControls=`
${ccAvatarV24('parents',p.id,p.name,true)}
${esc(p.name)}

Independent Athlete

${btn('Upload / change profile photo',`ccPickPhotoV24('parents','${p.id}')`,true)}${ccPhotoRecordV24('parents',p.id)?btn('Remove photo',`ccRemovePhotoV24('parents','${p.id}')`):''}
`; return ccV57PreviousMyProfile().replace('

Manage your account, contact information, and personal training goals.

','

Manage your account, contact information, and personal training goals.

'+photoControls); }; const ccV57PreviousRender=render; render=function(){ ccV57PreviousRender(); if(page==='Dashboard'){ const app=document.getElementById('app'); if(app&&!app.querySelector('.brandhero'))app.insertAdjacentHTML('afterbegin',hero()); } }; render(); /* V58: Independent Athlete dashboard displays their own editable profile photo immediately beneath the shared Champions Core header. Preview only. */ const ccV58PreviousDashboard=views['Dashboard']; views['Dashboard']=function(){ const body=ccV58PreviousDashboard(); if(!ccIndependentV50)return body; const {p}=ccV52Person(); if(!p)return body; const photo=ccAvatarV24('parents',p.id,p.name,true); const profileCard=panel('My profile',`
${photo}
${esc(p.name)}

Independent Athlete

${btn('View / edit my profile',"go('My Profile')",true)}${btn('Change profile picture',`ccV58ChangePhoto('${p.id}')`)}
`); return profileCard+body; }; function ccV58ChangePhoto(pid){ const mine=ccV52Person(); if(!ccIndependentV50||!mine.p||mine.p.id!==pid)return; const oldPage=page; try{page='My Profile';ccPickPhotoV24('parents',pid);}finally{page=oldPage;} } render(); /* V60: clickable dashboard lessons and role-specific lesson management (local demo). */ const ccV60Style=document.createElement('style');ccV60Style.textContent=`.cc-lesson-link{cursor:pointer;transition:background .16s,box-shadow .16s;outline:0}.cc-lesson-link:hover,.cc-lesson-link:focus-visible{background:rgba(200,155,88,.18)!important;box-shadow:inset 0 0 0 2px #c89b58}.cc-lesson-link td{cursor:pointer}.cc-lesson-focus{box-shadow:0 0 0 2px #c89b58!important;scroll-margin-top:18px}.cc-lesson-actions{display:flex;gap:8px;flex-wrap:wrap}`;document.head.appendChild(ccV60Style); function ccV60Owned(l){if(!l)return false;if(role==='admin')return true;if(ccIndependentV50){const {p,a}=ccV52Person();return !!p&&!!a&&l.parentId===p.id&&l.athleteId===a.id;}if(role==='parent')return l.parentId===parentId;return role==='athlete'&&l.athleteId===athleteId;} function ccV60ClickableRows(ls){return table(['When','Athlete','Facility','Status'],ls.slice(0,20).map(l=>`${esc(fmt(l.date,l.time))}${esc(aname(l.athleteId))}${esc(l.facility||'—')}${esc(l.status||'—')}`));} ccLessonRowsV36=function(ls){return ccV60ClickableRows(ls)}; function ccV60Open(lid){const l=db.lessons.find(x=>x.id===lid);if(!ccV60Owned(l))return alert('You cannot access this lesson.');window.ccV60SelectedLesson=lid;page='My Lessons';render();setTimeout(()=>{const el=document.getElementById('cc-lesson-'+lid);if(el){el.classList.add('cc-lesson-focus');el.scrollIntoView({block:'center',behavior:'smooth'});}},35);} function ccV60Cancel(lid){const l=db.lessons.find(x=>x.id===lid);if(!ccV60Owned(l)||role==='athlete'||role==='admin')return alert('This action is not available.');if(!future(l.date,l.time)||['Cancelled','Deleted'].includes(l.status))return alert('Only upcoming active lessons can be cancelled.');const hours=(new Date(l.date+'T'+(l.time||'00:00')).getTime()-Date.now())/3600000;const msg=hours>=24?'Request cancellation? If approved, you can discuss facility-specific Champions Credits or a refund with your coach.':'This is within 24 hours. Any cancellation, credit, or refund is at the coach’s discretion. Send a cancellation request?';if(!confirm(msg))return;db.requests ||= [];if(db.requests.some(r=>r.lessonId===lid&&r.type==='Cancellation'&&r.status==='Pending approval'))return alert('A cancellation request is already pending.');db.requests.push({id:id(),type:'Cancellation',lessonId:lid,parentId:l.parentId,athleteId:l.athleteId,facility:l.facility,date:l.date,time:l.time,status:'Pending approval',notes:hours>=24?'Cancellation requested at least 24 hours in advance':'Cancellation requested within 24 hours'});save();render();alert('Cancellation request sent to the coach (browser-local demo). Your booking remains active until approved.');} function ccV60Delete(lid){if(role!=='admin')return;deleteLessonV18(lid);} function ccV60Actions(l){if(!future(l.date,l.time)||['Cancelled','Deleted','Available'].includes(l.status))return 'No actions available';if(role==='athlete')return 'View only';if(role==='admin')return `
${btn('Reschedule',`reschedule('${l.id}')`)}${btn('Cancel',`lessonCancelV17('${l.id}')`)}${btn('Delete',`ccV60Delete('${l.id}')`)}
`;return `
${btn('Reschedule',`reschedule('${l.id}')`)}${btn('Cancel',`ccV60Cancel('${l.id}')`)}
`;} const ccV60PriorLessons=views['My Lessons']; views['My Lessons']=function(){if(role==='admin'){const ls=db.lessons.filter(l=>l.status!=='Available').sort((a,b)=>(b.date+b.time).localeCompare(a.date+a.time));return panel('Manage lessons',`

Select a lesson to manage its schedule, cancellation or deletion.

`+ls.map(l=>`
${esc(fmt(l.date,l.time))} · ${esc(aname(l.athleteId))} · ${esc(l.facility)} · ${esc(l.status)}
${ccV60Actions(l)}
`).join(''));} const ls=db.lessons.filter(l=>ccV60Owned(l)&&l.status!=='Available').sort((a,b)=>(b.date+b.time).localeCompare(a.date+a.time));if(!ls.length)return ccV60PriorLessons();return panel('My lessons — upcoming and past',`

${role==='athlete'?'View the date, time and location of your lessons.':'Select an upcoming lesson to request cancellation or rescheduling. Requests are subject to coach approval and the cancellation policy.'}

`+ls.map(l=>`
${esc(fmt(l.date,l.time))}

${esc(aname(l.athleteId))} · ${esc(l.facility||'—')} · ${esc(l.duration||30)} minutes · ${esc(l.status)}

${ccV60Actions(l)}
`).join(''));}; const ccV60OldDashboard=views['Dashboard']; views['Dashboard']=function(){const body=ccV60OldDashboard();if(!ccIndependentV50)return body;const {p,a}=ccV52Person();if(!p||!a)return body;const mine=db.lessons.filter(l=>l.parentId===p.id&&l.athleteId===a.id&&ccActiveLessonV36(l));return body+ccUpcomingV36(mine,'My upcoming lessons');}; render(); /* V61: role-targeted lesson change alerts (local preview only). */ function ccV61Event(l,kind,actor,extra=''){ if(!l)return; db.ccLessonEvents ||= []; db.ccLessonEvents.unshift({id:id(),lessonId:l.id,parentId:l.parentId,athleteId:l.athleteId,kind,actor,extra,date:new Date().toISOString(),when:fmt(l.date,l.time),facility:l.facility||'—',athlete:aname(l.athleteId)}); db.ccLessonEvents=db.ccLessonEvents.slice(0,150); save(); } function ccV61Visible(e){ if(role==='admin')return e.actor!=='admin'; if(role==='athlete')return e.athleteId===athleteId&&e.actor==='admin'; if(ccIndependentV50){const {p,a}=ccV52Person();return !!p&&!!a&&e.parentId===p.id&&e.athleteId===a.id&&e.actor==='admin';} return role==='parent'&&e.parentId===parentId&&e.actor==='admin'; } function ccV61Alerts(){ const events=(db.ccLessonEvents||[]).filter(ccV61Visible).slice(0,12); if(!events.length)return ''; return panel('Lesson changes & notifications · '+events.length,`

Only changes relevant to your account appear here. These are browser-local demo alerts, not delivered messages.

`+events.map(e=>{ const title=e.kind==='cancel-request'?'Cancellation requested':e.kind==='reschedule-request'?'Reschedule requested':e.kind==='cancelled'?'Lesson cancelled':e.kind==='deleted'?'Lesson deleted':e.kind==='rescheduled'?'Lesson rescheduled':'Lesson updated'; const detail=role==='athlete'?'Your coach '+(e.kind==='cancelled'?'cancelled':e.kind==='deleted'?'removed':'changed')+' this lesson.':(e.actor==='admin'?'Coach':'Parent / independent athlete')+' · '+e.athlete+' · '+e.when+' · '+e.facility; const canOpen=e.kind!=='deleted'&&db.lessons.some(l=>l.id===e.lessonId&&ccV60Owned(l)); return `
${esc(title)}

${esc(detail)}

${e.extra?`

${esc(e.extra)}

`:''}${esc(new Date(e.date).toLocaleString('en-US'))}${canOpen?`
${btn('View lesson',`ccV60Open('${e.lessonId}')`)}
`:''}
`; }).join('')); } const ccV61OldDash=views['Dashboard']; views['Dashboard']=function(){return ccV61Alerts()+ccV61OldDash();}; const ccV61OldCancel=ccV60Cancel; ccV60Cancel=function(lid){const before=(db.requests||[]).filter(r=>r.lessonId===lid&&r.type==='Cancellation').length;ccV61OldCancel(lid);const l=db.lessons.find(x=>x.id===lid);if(l&&(db.requests||[]).filter(r=>r.lessonId===lid&&r.type==='Cancellation').length>before){ccV61Event(l,'cancel-request','family');render();}}; const ccV61OldReschedule=saveReschedule; saveReschedule=function(f){const lid=f.dataset.id,l=db.lessons.find(x=>x.id===lid),before=(db.requests||[]).filter(r=>r.lessonId===lid&&r.type==='Reschedule').length;ccV61OldReschedule(f);if(l&&(db.requests||[]).filter(r=>r.lessonId===lid&&r.type==='Reschedule').length>before){ccV61Event(l,'reschedule-request',role==='admin'?'admin':'family',`Proposed: ${val(f,'date')} ${val(f,'time')}`);render();}}; const ccV61OldSaveCancel=saveCancelV17; saveCancelV17=function(f){const l=db.lessons.find(x=>x.id===f.dataset.id),was=l?.status;ccV61OldSaveCancel(f);if(l&&was!=='Cancelled'&&l.status==='Cancelled'){ccV61Event(l,'cancelled','admin',l.cancelMessage||'');render();}}; const ccV61OldDelete=deleteLessonV18; deleteLessonV18=function(lid){const l=db.lessons.find(x=>x.id===lid);ccV61OldDelete(lid);if(l&&!db.lessons.some(x=>x.id===lid)){ccV61Event(l,'deleted','admin');render();}}; render(); /* V62: per-preview-account read, dismiss and history controls. Browser-local only. */ function ccV62AccountKey(){if(role==='admin')return 'admin';if(ccIndependentV50){const x=ccV52Person();return 'independent:'+String(x.p?.id||'none');}return role+':'+String(role==='parent'?parentId:athleteId);} function ccV62Events(){return (db.ccLessonEvents||[]).filter(ccV61Visible);} function ccV62Has(e,field){return Array.isArray(e[field])&&e[field].includes(ccV62AccountKey());} function ccV62Mark(eid,field){const e=(db.ccLessonEvents||[]).find(x=>x.id===eid);if(!e||!ccV61Visible(e)||!['ccReadBy','ccDismissedBy'].includes(field))return;e[field] ||= [];const key=ccV62AccountKey();if(!e[field].includes(key))e[field].push(key);if(field==='ccDismissedBy'){e.ccReadBy ||= [];if(!e.ccReadBy.includes(key))e.ccReadBy.push(key);}save();render();} function ccV62MarkAll(){ccV62Events().forEach(e=>{if(ccV62Has(e,'ccDismissedBy'))return;e.ccReadBy ||= [];if(!ccV62Has(e,'ccReadBy'))e.ccReadBy.push(ccV62AccountKey());});save();render();} function ccV62Title(e){return ({'cancel-request':'Cancellation requested','reschedule-request':'Reschedule requested','cancelled':'Lesson cancelled','deleted':'Lesson deleted','rescheduled':'Lesson rescheduled'})[e.kind]||'Lesson updated';} function ccV62Item(e,history){const isRead=ccV62Has(e,'ccReadBy'),dismissed=ccV62Has(e,'ccDismissedBy');const detail=role==='athlete'?'Your coach '+(e.kind==='cancelled'?'cancelled':e.kind==='deleted'?'removed':'changed')+' this lesson.':(e.actor==='admin'?'Coach':'Parent / independent athlete')+' · '+e.athlete+' · '+e.when+' · '+e.facility;const canOpen=e.kind!=='deleted'&&db.lessons.some(l=>l.id===e.lessonId&&ccV60Owned(l));return `
${esc(ccV62Title(e))} ${!isRead?'• Unread':''}${dismissed&&history?' · Dismissed':''}

${esc(detail)}

${e.extra?`

${esc(e.extra)}

`:''}${esc(new Date(e.date).toLocaleString('en-US'))}
${canOpen?btn('View lesson',`ccV62Open('${e.id}','${e.lessonId}')`):''}${!isRead?btn('Mark read',`ccV62Mark('${e.id}','ccReadBy')`):''}${!dismissed?btn('Dismiss',`ccV62Mark('${e.id}','ccDismissedBy')`):''}
`;} function ccV62Open(eid,lid){ccV62Mark(eid,'ccReadBy');ccV60Open(lid);} ccV61Alerts=function(){const events=ccV62Events().filter(e=>!ccV62Has(e,'ccReadBy')&&!ccV62Has(e,'ccDismissedBy'));if(!events.length)return '';return panel('Lesson notifications · '+events.length+ ' unread',`

New lesson changes that need your attention. Mark read or dismiss to clear this dashboard.

${btn('Mark all read','ccV62MarkAll()')}${btn('Notification history',"go('Notification History')")}
`+events.slice(0,12).map(e=>ccV62Item(e,false)).join('')+(events.length>12?`

${events.length-12} more unread notifications in history.

`:''));}; views['Notification History']=function(){const events=ccV62Events();return panel('Lesson notification history',`

Review previous lesson requests and coach changes. Reading or dismissing an alert does not change the booking.

${btn('Back to dashboard',"go('Dashboard')")}${btn('Mark all read','ccV62MarkAll()')}
`+(events.length?events.map(e=>ccV62Item(e,true)).join(''):'

No lesson notifications yet.

'));}; const ccV62PreviousDash=views['Dashboard']; views['Dashboard']=function(){const html=ccV62PreviousDash();const all=ccV62Events();return html+(all.length?panel('Notification history',`

${all.length} previous lesson change${all.length===1?'':'s'} saved in this browser.

${btn('View notification history',"go('Notification History')")}
`):'');}; render(); /* V69: authentication shell around the untouched V68 portal. Demo records remain browser-local. */ const cc69Base='https://ipsozttjblqqsyhnpckp.supabase.co'; const cc69Key='sb_publishable_a_Tx9adYbcv2P2REo842gA_icuQTMn7'; const cc69SessionKey='cc_auth_session_v63'; let cc69Identity=null; /* V80: 15-minute inactivity limit. A session must have an activity timestamp set by a successful login; pre-v80 saved sessions must sign in again. */ const cc80IdleKey='cc_auth_last_activity_v80'; const cc80IdleLimit=15*60*1000; let cc80IdleStarted=false,cc80Expired=false,cc80LastWrite=0; function cc80IsIdle(){ const last=Number(localStorage.getItem(cc80IdleKey)); return !Number.isFinite(last)||last<=0||last>Date.now()+60000||Date.now()-last>=cc80IdleLimit; } function cc80Expire(){ if(cc80Expired)return; cc80Expired=true; localStorage.removeItem(cc69SessionKey); localStorage.removeItem(cc80IdleKey); cc69Identity=null; cc69Gate('Your session expired after 15 minutes of inactivity. Redirecting to sign in…'); location.replace('/login.html?expired=1'); } function cc80Check(){ if(cc80Expired)return; if(!localStorage.getItem(cc69SessionKey)||cc80IsIdle())cc80Expire(); } function cc80Start(){ if(cc80IdleStarted||cc80Expired)return; cc80IdleStarted=true; const activity=()=>{ if(document.visibilityState==='hidden'||cc80Expired)return; if(cc80IsIdle()){cc80Expire();return;} if(Date.now()-cc80LastWrite<10000)return; cc80LastWrite=Date.now();localStorage.setItem(cc80IdleKey,String(cc80LastWrite)); }; ['pointerdown','keydown','touchstart','scroll'].forEach(name=>document.addEventListener(name,activity,{passive:true})); document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')cc80Check();}); window.addEventListener('focus',cc80Check); window.addEventListener('pageshow',cc80Check); window.addEventListener('storage',event=>{if(event.key===cc69SessionKey&&!event.newValue)cc80Expire();if(event.key===cc80IdleKey&&!event.newValue)cc80Expire();}); setInterval(cc80Check,10000); cc80Check(); } function cc69Gate(message){const g=document.getElementById('cc69Gate');if(g){g.style.display='grid';document.getElementById('cc69GateText').textContent=message;}} function cc69Unlock(){const g=document.getElementById('cc69Gate');if(g)g.style.display='none';} async function cc69Api(path,token,opts={}){ const controller=new AbortController();const timer=setTimeout(()=>controller.abort(),12000); let r;try{r=await fetch(cc69Base+path,{...opts,signal:controller.signal,headers:{apikey:cc69Key,...(token?{Authorization:'Bearer '+token}:{}),...(opts.headers||{})}});}finally{clearTimeout(timer);} const j=await r.json().catch(()=>({}));if(!r.ok)throw Error(j.message||j.msg||j.error_description||j.error||'HTTP '+r.status);return j; } async function cc69Validate(){ // Version 77: Authentication is verified from the saved Supabase session, not a tab-only flag. // sessionStorage does not reliably survive all mobile browser navigation contexts. let s;try{s=JSON.parse(localStorage.getItem(cc69SessionKey)||'null')}catch{} if(!s?.access_token||!s?.refresh_token){location.replace('/login.html');return;} if(cc80IsIdle()){cc80Expire();return;} try{ if(!s.expires_at||s.expires_at div');if(box&&!document.getElementById('cc77Retry')){const b=document.createElement('button');b.id='cc77Retry';b.textContent='Retry secure login';b.style.cssText='display:block;margin:18px auto;padding:12px 20px;border:1px solid #c89b58;border-radius:8px;background:#c89b58;color:#15161c;font:inherit;cursor:pointer';b.onclick=()=>{b.remove();cc69Gate('Checking your secure login…');cc69Validate()};box.appendChild(b);}const link=document.querySelector('#cc69Gate a');if(link)link.textContent='Return to login';} } async function cc69Signout(){ let s;try{s=JSON.parse(localStorage.getItem(cc69SessionKey)||'null')}catch{} localStorage.removeItem(cc69SessionKey);localStorage.removeItem(cc80IdleKey);sessionStorage.removeItem('cc_hub_entry_v72');cc69Gate('Signing out…'); if(s?.access_token){try{await cc69Api('/auth/v1/logout',s.access_token,{method:'POST'})}catch{}} location.replace('/login.html'); } cc69Validate(); /* V70: Keep the original portal and Settings intact; add account controls there. */ const cc70SettingsOriginal=views['Settings']; views['Settings']=function(){ const original=typeof cc70SettingsOriginal==='function'?cc70SettingsOriginal():''; if(!cc69Identity)return original; // Admin preview may switch roles, but that must not expose the admin identity on fictional profiles. // Only show the signed-in admin's identity while actually viewing the Admin profile. const showAdminIdentity=role==='admin' && cc69Identity.role==='admin'; const accountLabel=showAdminIdentity ? '

'+esc(String(cc69Identity.name||'Champions Core Administrator'))+' · Admin

' : '

Sign out

'; return original+panel('Account & security', accountLabel+ '

You are signed in using Supabase. Signing out ends your current session on this browser and returns you to the login page.

'+ '
'); }; // Make the Settings action accessible to all roles without changing admin navigation. const cc70SettingsNav=renderNav; renderNav=function(){ cc70SettingsNav(); if(role!=='admin'){ const nav=document.getElementById('nav'); if(nav&&!Array.from(nav.querySelectorAll('button')).some(b=>b.textContent.trim()==='Settings')){ const group=document.createElement('details');group.open=true; const title=document.createElement('summary');title.textContent='Account';group.appendChild(title); const b=document.createElement('button');b.textContent='Settings';b.onclick=()=>go('Settings');group.appendChild(b);nav.appendChild(group); } } }; /* V71: Account & Security must be reachable from every real account and demo preview. */ const cc71PreviousSettings = views['Settings']; views['Settings'] = function(){ const html=typeof cc71PreviousSettings==='function'?cc71PreviousSettings():''; if(cc69Identity) return html; // V70 already adds authenticated sign-out panel. return html+panel('Account & security','

Sign in to manage your Champions Core HUB session.

'); }; const cc71PreviousNav=renderNav; renderNav=function(){ cc71PreviousNav(); const nav=document.getElementById('nav'); if(!nav)return; const hasSettings=Array.from(nav.querySelectorAll('button')).some(b=>b.textContent.trim()==='Settings'); if(!hasSettings){ const details=document.createElement('details');details.open=true; const summary=document.createElement('summary');summary.textContent='Account';details.appendChild(summary); const button=document.createElement('button');button.type='button';button.textContent='Settings';button.className=page==='Settings'?'active':'';button.onclick=()=>go('Settings');details.appendChild(button);nav.appendChild(details); } }; // A user switching between preview roles retains the same real Supabase session. // The Account & Security panel always belongs to the signed-in identity. /* V73: Business payment destinations (browser-local demonstration). In production, move configuration to a protected Supabase business-settings table; do not treat browser localStorage as authoritative across users/devices. */ const cc73PaymentKey='cc_hub_payment_destinations_v73'; function cc73PaymentConfig(){ let v={};try{v=JSON.parse(localStorage.getItem(cc73PaymentKey)||'{}')||{}}catch{} return {cashappUrl:String(v.cashappUrl||''),cashappTag:String(v.cashappTag||''),venmoUrl:String(v.venmoUrl||''),venmoTag:String(v.venmoTag||''),zelle:String(v.zelle||'')}; } function cc73SafeLink(raw,provider){ try{const u=new URL(raw);if(u.protocol!=='https:')return ''; const h=u.hostname.toLowerCase(); if(provider==='Cash App' && !['cash.app','www.cash.app'].includes(h))return ''; if(provider==='Venmo' && !['venmo.com','www.venmo.com'].includes(h))return ''; return u.href; }catch{return ''} } function cc73PaymentOptions(){ const v=cc73PaymentConfig(); return [['Cash App',v.cashappTag||'Payment details not yet configured'],['Venmo',v.venmoTag||'Payment details not yet configured'],['Zelle',v.zelle||'Payment details not yet configured']]; } function cc73PaymentLink(name){ const v=cc73PaymentConfig(),url=name==='Cash App'?cc73SafeLink(v.cashappUrl,name):name==='Venmo'?cc73SafeLink(v.venmoUrl,name):''; return url?`
Open ${esc(name)} ↗`:''; } function cc73SavePaymentSettings(form){ if(role!=='admin'||cc69Identity?.role!=='admin')return alert('Only the signed-in administrator can change payment destinations.'); const cashappUrl=val(form,'cashappUrl'),venmoUrl=val(form,'venmoUrl'); if(cashappUrl&&!cc73SafeLink(cashappUrl,'Cash App'))return alert('Enter a valid HTTPS Cash App link on cash.app.'); if(venmoUrl&&!cc73SafeLink(venmoUrl,'Venmo'))return alert('Enter a valid HTTPS Venmo link on venmo.com.'); const zelle=val(form,'zelle'); if(zelle.length>150)return alert('Zelle contact is too long.'); const config={cashappUrl,cashappTag:val(form,'cashappTag'),venmoUrl,venmoTag:val(form,'venmoTag'),zelle}; localStorage.setItem(cc73PaymentKey,JSON.stringify(config)); alert('Payment destinations saved in this browser (demo). Parent and independent-athlete payment options on this browser now use these values.'); render(); } function cc73AdminPayments(){ if(role!=='admin'||cc69Identity?.role!=='admin')return ''; const v=cc73PaymentConfig(); return panel('Payments',`

Manage the payment links and Zelle contact displayed to parents and independent athletes at checkout. These settings are currently saved only in this browser; they will not synchronize to customers on other devices until connected to Supabase. Do not accept real payments through the demonstration checkout.

${field('Cash App payment link (https://cash.app/...)','cashappUrl',v.cashappUrl,'url','placeholder="https://cash.app/$YourTag"')} ${field('Cash App $cashtag','cashappTag',v.cashappTag,'text','placeholder="$YourTag"')} ${field('Venmo profile/payment link (https://venmo.com/...)','venmoUrl',v.venmoUrl,'url','placeholder="https://venmo.com/YourName"')} ${field('Venmo @username','venmoTag',v.venmoTag,'text','placeholder="@YourName"')} ${field('Zelle email or enrolled mobile number','zelle',v.zelle,'text','placeholder="payments@example.com"')}

Zelle uses the enrolled email address or phone number, not a clickable payment link. Leave any method blank to display “Payment details not yet configured.”

`); } const cc73PriorSettings=views['Settings']; views['Settings']=function(){return (typeof cc73PriorSettings==='function'?cc73PriorSettings():'')+cc73AdminPayments()}; /* V74: Collapsible Settings for all account types; sign out is always last. Payment tags are the visible links; destinations remain browser-local demo data. */ function cc74PaymentTag(name){ const v=cc73PaymentConfig(); const tag=name==='Cash App'?v.cashappTag:name==='Venmo'?v.venmoTag:v.zelle; if(!tag)return 'Not configured'; const raw=name==='Cash App'?v.cashappUrl:name==='Venmo'?v.venmoUrl:''; const url=cc73SafeLink(raw,name); return url?`${esc(tag)} ↗`:`${esc(tag)}`; } // The payment chooser already uses cc73PaymentOptions() and cc73PaymentLink(). // Display ONLY the clickable handle in the payment choice, not the raw URL or a separate Open link. cc73PaymentOptions=function(){ const v=cc73PaymentConfig(); return [['Cash App',v.cashappTag||'Not configured'],['Venmo',v.venmoTag||'Not configured'],['Zelle',v.zelle||'Not configured']]; }; cc73PaymentLink=function(name){ const v=cc73PaymentConfig(); const raw=name==='Cash App'?v.cashappUrl:name==='Venmo'?v.venmoUrl:''; const url=cc73SafeLink(raw,name); // A transparent stretched link overlays the text handle in the payment choice, while the radio remains clickable. return url?`
${esc(name==='Cash App'?v.cashappTag:v.venmoTag)} ↗`:''; }; // Replace duplicate descriptive text with a single clickable handle in the checkout card. const cc74OldPaymentModal=paymentModal; paymentModal=function(pid){ cc74OldPaymentModal(pid); const cards=document.querySelectorAll('#modalroot label.panel'); cards.forEach(card=>{ const name=card.querySelector('input[name="paymethod"]')?.value; if(name!=='Cash App'&&name!=='Venmo')return; const a=card.querySelector('a.cc74-payment-link'); if(!a)return; const small=card.querySelector('small'); if(small)small.remove(); const br=a.previousElementSibling; if(br&&br.tagName==='BR')br.remove(); }); }; function cc74CustomerPaymentMethods(){ return panel('Ways to pay',`

Payment destinations supplied by Champions Core. Select a handle to open Cash App or Venmo. Zelle shows the enrolled contact to enter in your bank app.

${['Cash App','Venmo','Zelle'].map(n=>``).join('')}

Preview only: payment information is stored in this browser and does not synchronize to other customers’ devices yet. Do not send real payments from the demonstration.

`); } const cc74PriorMyPayments=views['My Payments']; views['My Payments']=function(){return (typeof cc74PriorMyPayments==='function'?cc74PriorMyPayments():'')+((role==='parent'||role==='athlete')?cc74CustomerPaymentMethods():'')}; // Parent/independent athlete payment panels use the same saved configuration as the Admin Settings form. function cc74CollapseSettings(){ if(page!=='Settings')return; const root=document.getElementById('app');if(!root)return; const children=Array.from(root.children); const security=[]; for(const child of children){ if(!child.classList.contains('panel'))continue; const heading=child.querySelector('h2,h3,h4'); const title=(heading?.textContent||'Settings section').trim(); if(/^Account\s*&\s*security$/i.test(title)){security.push(child);continue} const details=document.createElement('details');details.className='cc74-settings-accordion'; const summary=document.createElement('summary');summary.textContent=title;details.appendChild(summary); if(heading)heading.remove(); child.parentNode.insertBefore(details,child);details.appendChild(child); } for(const child of security){ const heading=child.querySelector('h2,h3,h4');if(heading)heading.remove(); const details=document.createElement('details');details.className='cc74-settings-accordion cc74-security'; const summary=document.createElement('summary');summary.textContent='Account & security';details.appendChild(summary); root.appendChild(details);details.appendChild(child); } } const cc74PreviousRender=render; render=function(){cc74PreviousRender();cc74CollapseSettings()}; const cc74PreviousGo=go; go=function(x){cc74PreviousGo(x);if(x==='Settings')document.querySelector('main')?.scrollTo({top:0,behavior:'instant'})};