SIGN IN
Our Story
Every website has a story, and your visitors want to hear yours. This space is a great opportunity to give a full background on who you are, what your team does, and what your site has to offer. Double click on the text to start editing your content and make sure to add all the relevant details you want site visitors to know.
If you’re a business, talk about how you started and share your professional journey. Explain your core values, your commitment to customers, and how you stand out from the crowd. Add a photo, gallery, or video for even more engagement.
<!doctype html><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<title>Pasodos Portal — Wix Embed</title>
<style>
:root{
--bg:#ffffff; --card:#ffffff; --text:#111827; --muted:#6b7280;
--accent:#bb2649; --border:#e5e7eb; --pill:#f3f4f6;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text);font:16px/1.45 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Ubuntu,system-ui}
.container{max-width:960px;margin:0 auto;padding:12px}
h1{font-size:22px;margin:6px 0 10px 0}
h2{font-size:18px;margin:10px 0 8px 0}
p{margin:6px 0}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:12px;margin:10px 0}
.row{display:flex;gap:10px;flex-wrap:wrap}
.col{flex:1 1 260px}
.input,select,textarea{width:100%;padding:10px;border:1px solid var(--border);border-radius:10px;background:#fff;color:var(--text)}
.btn{display:inline-block;padding:10px 14px;border-radius:10px;border:1px solid var(--border);background:#fff;color:var(--text);cursor:pointer;text-decoration:none}
.btn:hover{border-color:var(--text)}
.btn.primary{background:var(--accent);color:#fff;border-color:var(--accent)}
.badge{display:inline-block;padding:2px 8px;border-radius:999px;background:var(--pill);font-size:12px;color:#111}
.table{width:100%;border-collapse:collapse}
.table th,.table td{border-bottom:1px solid var(--border);padding:8px;text-align:left}
.small{font-size:12px;color:var(--muted)}
.nav{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
.nav a{padding:6px 10px;border:1px solid var(--border);border-radius:999px;text-decoration:none;color:var(--text)}
.nav a.active{background:var(--accent);border-color:var(--accent);color:#fff}
.notice{background:#fff8e1;border:1px solid #ffe58f}
.success{background:#ecfdf5;border:1px solid #a7f3d0}
</style>
<div class=container>
<div class="nav" id="topNav"></div>
<div id="app"></div>
<div class="small" style="margin-top:12px">Prototype — data is saved only in your browser (localStorage).</div>
</div>
<script>
const CATALOG = [
{"code":"preballet-fri","name":"PreBallet (Fri)","monthly":70,"term1":220,"term2":165,"term3":180,"full":480},
{"code":"preballet-mw","name":"PreBallet (Mon, Wed)","monthly":80,"term1":260,"term2":195,"term3":210,"full":585},
{"code":"ballet1-fri","name":"Ballet 1 (Fri)","monthly":70,"term1":220,"term2":165,"term3":180,"full":480},
{"code":"ballet1-mw","name":"Ballet 1 (Mon, Wed)","monthly":80,"term1":260,"term2":195,"term3":210,"full":585},
{"code":"ballet2-tt","name":"Ballet 2 (Tues, Thurs)","monthly":90,"term1":300,"term2":225,"term3":240,"full":670},
{"code":"ballet3-tt","name":"Ballet 3 (Tues, Thurs)","monthly":90,"term1":300,"term2":225,"term3":240,"full":670},
{"code":"ballet4-tt","name":"Ballet 4 (Tues, Thurs)","monthly":90,"term1":300,"term2":225,"term3":240,"full":670},
{"code":"elite1","name":"Ballet Elite 1 (Mon - Fri)","monthly":125,"term1":440,"term2":330,"term3":345,"full":1000},
{"code":"elite-inter","name":"Ballet Elite Inter (Mon, Wed, Fri)","monthly":130,"term1":460,"term2":345,"term3":360,"full":1040},
{"code":"advanced-ttf","name":"Ballet Advanced (Tues, Thurs, Fri)","monthly":145,"term1":520,"term2":345,"term3":360,"full":1100},
{"code":"saturday-supp","name":"The Saturday Class (supplement)","monthly":70,"term1":220,"term2":165,"term3":165,"full":500,"supplement":true},
{"code":"adult-x1","name":"Ballet Adultos (x1)","monthly":70,"term1":220,"term2":165,"term3":180,"full":505},
{"code":"adult-x2","name":"Ballet Adultos (x2)","monthly":95,"term1":320,"term2":240,"term3":255,"full":720},
{"code":"mk1","name":"Modern Kids 1 (Mon, Wed)","monthly":85,"term1":280,"term2":210,"term3":225,"full":630},
{"code":"mk2","name":"Modern Kids 2 (Tues, Thurs)","monthly":85,"term1":280,"term2":210,"term3":225,"full":630},
{"code":"contemp","name":"Contemp (Mon, Wed, Fri)","monthly":120,"term1":420,"term2":315,"term3":330,"full":940},
{"code":"jazz-supp","name":"Jazz (supplement)","monthly":85,"term1":280,"term2":210,"term3":225,"full":630,"supplement":true},
{"code":"combo1","name":"Combo 1: Ballet 1 + MK1","monthly":130,"term1":460,"term2":345,"term3":360,"full":1150,"combo":true},
{"code":"combo2","name":"Combo 2: Ballet 2/3/4 + MK2","monthly":136,"term1":464,"term2":348,"term3":363,"full":1160,"combo":true},
{"code":"combo3","name":"Combo 3: Elite 1 + MK2","monthly":164,"term1":575,"term2":431,"term3":446,"full":1436,"combo":true},
{"code":"combo4a","name":"Combo 4: Elite 2 + B4","monthly":150,"term1":519,"term2":389,"term3":404,"full":1297,"combo":true},
{"code":"combo4b","name":"Combo 4: Elite 2 + B4 + MK","monthly":195,"term1":699,"term2":525,"term3":540,"full":1749,"combo":true},
{"code":"combo5","name":"Combo 5: Inter + Contemp","monthly":188,"term1":671,"term2":503,"term3":518,"full":1676,"combo":true},
{"code":"combo6","name":"Combo 6: Adv + Inter","monthly":248,"term1":908,"term2":530,"term3":545,"full":1968,"combo":true},
{"code":"fullcombo","name":"FULL COMBO — all you can dance!","monthly":350,"term1":1200,"term2":900,"term3":915,"full":3000,"combo":true}
];
const STORE_KEY = 'pasodos_wix_portal_v1';
const DEFAULTS = {
settings: { matriculationFee: 50 },
auth: { currentUserId: null, users: [{ id:'u_admin', role:'admin', name:'Admin', email:'admin@demo.app', phone:'', password:'admin123' }]},
clients: [],
students: [],
enrollments: [], // {id, studentId, planCode, billing:'monthly'|'term1'|'term2'|'term3'|'full', payment:'Bank transfer'|'Cash', status}
attendance: [] // {id, planCode, dateISO, studentId, status}
};
let state = load();
function load(){ try{ const raw = localStorage.getItem(STORE_KEY); return raw? JSON.parse(raw) : seed(); }catch(e){ return seed(); } }
function seed(){ const s = JSON.parse(JSON.stringify(DEFAULTS)); return s; }
function save(){ localStorage.setItem(STORE_KEY, JSON.stringify(state)); }
function uid(p='id'){ return p+'_'+Math.random().toString(36).slice(2,9); }
function today(){ return new Date().toISOString().slice(0,10); }
function currentUser(){ return state.auth.users.find(u => u.id===state.auth.currentUserId) || null; }
function $(sel){ return document.querySelector(sel); }
function el(tag, attrs={}, kids=[]){ const e = document.createElement(tag); for(const [k,v] of Object.entries(attrs)){ if(k==='class') e.className=v; else if(k==='html') e.innerHTML=v; else if(k.startsWith('on') && typeof v==='function') e.addEventListener(k.slice(2), v); else e.setAttribute(k, v); } (Array.isArray(kids)?kids:[kids]).filter(Boolean).forEach(c => e.appendChild(typeof c==='string'?document.createTextNode(c):c)); return e; }
const routes = {};
function route(path, fn){ routes[path]=fn; }
function go(path){ location.hash = '#'+path; }
function setNav(items){ const nav = $('#topNav'); nav.innerHTML=''; items.forEach(i => { const a = el('a', { href:'#'+i.path, class: (location.hash==='#'+i.path?'active':'') }, i.label); nav.appendChild(a); }); }
function clear(){ const a = $('#app'); a.innerHTML=''; return a; }
// HOME
route('home', () => {
const a = clear(); const u = currentUser(); if(u){ go(u.role==='admin'?'admin':'client'); return; }
setNav([{label:'Login', path:'home'}]);
a.appendChild(el('div',{class:'card'},[
el('h1',{},'Welcome'),
el('p',{},'Create a client account or use the admin demo login.'),
el('div',{class:'row'},[
el('div',{class:'col'},[
el('h2',{},'Client sign up'),
el('input',{class:'input',id:'su_name',placeholder:'Full name'}),
el('input',{class:'input',id:'su_email',placeholder:'Email',type:'email'}),
el('input',{class:'input',id:'su_phone',placeholder:'Phone'}),
el('input',{class:'input',id:'su_pass',placeholder:'Password',type:'password'}),
el('label',{},[ el('input',{type:'checkbox',id:'su_self'}), ' I am also a student (Adult Ballet, etc.)' ]),
el('div',{}, el('button',{class:'btn primary', onClick:()=>{
const name=$('#su_name').value.trim();
const email=($('#su_email').value||'').trim().toLowerCase();
const phone=$('#su_phone').value.trim();
const pass=$('#su_pass').value;
const self=$('#su_self').checked;
if(!name||!email||!pass) return alert('Name, email, password are required');
if(state.auth.users.find(u=>u.email===email)) return alert('Email already registered');
const userId=uid('u');
state.auth.users.push({id:userId,role:'client',name,email,phone,password:pass});
const clientId=uid('cli');
state.clients.push({id:clientId,userId,name,email,phone,isReturning:false,matriculationPaid:false});
if(self){ state.students.push({id:uid('stu'),clientId,name,isClientSelf:true}); }
state.auth.currentUserId=userId; save(); go('client');
}},'Create account'))
]),
el('div',{class:'col'},[
el('h2',{},'Login'),
el('input',{class:'input',id:'li_email',placeholder:'Email',type:'email'}),
el('input',{class:'input',id:'li_pass',placeholder:'Password',type:'password'}),
el('div',{}, el('button',{class:'btn', onClick:()=>{
const email=($('#li_email').value||'').trim().toLowerCase();
const pass=$('#li_pass').value;
const u=state.auth.users.find(x=>x.email===email && x.password===pass);
if(!u) return alert('Invalid credentials');
state.auth.currentUserId=u.id; save(); go(u.role==='admin'?'admin':'client');
}},'Login')),
el('p',{class:'small'},'Admin demo: admin@demo.app / admin123')
])
])
]));
});
// CLIENT
route('client', () => {
const u = currentUser(); if(!u||u.role!=='client'){ go('home'); return; }
setNav([{label:'My dashboard',path:'client'},{label:'Enroll',path:'enroll'},{label:'Logout',path:'logout'}]);
const a = clear();
const client = state.clients.find(c=>c.userId===u.id);
a.appendChild(el('div',{class:'card'},[
el('h1',{},'Profile'),
el('div',{},`Name: ${client.name}`),
el('div',{},`Email: ${client.email}`),
el('div',{},`Phone: ${client.phone||'-'}`),
el('div',{},`Returning client: ${client.isReturning?'Yes':'No (matriculation due)'}`),
el('div',{}, el('button',{class:'btn', onClick:()=>{ client.isReturning=!client.isReturning; if(client.isReturning) client.matriculationPaid=true; save(); route('client')(); }}, client.isReturning?'Set as new (matriculation)':'Set as returning'))
]));
const students = state.students.filter(s=>s.clientId===client.id);
const scard = el('div',{class:'card'},[ el('h2',{},'Students') ]);
if(students.length===0) scard.appendChild(el('div',{},'No students yet. Add one below.'));
else{
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Name'), el('th',{},'Enrollments') ]) ]);
students.forEach(s => {
const count = state.enrollments.filter(en=>en.studentId===s.id && en.status==='active').length;
t.appendChild(el('tr',{},[ el('td',{}, s.name + (s.isClientSelf?' (you)':'')), el('td',{}, String(count)) ]));
});
scard.appendChild(t);
}
scard.appendChild(el('div',{},[
el('input',{class:'input',id:'new_stu',placeholder:'Student full name'}),
el('div',{},el('button',{class:'btn',onClick:()=>{
const name=$('#new_stu').value.trim(); if(!name) return alert('Name required');
state.students.push({id:uid('stu'),clientId:client.id,name,isClientSelf:false}); save(); route('client')();
}},'Add student'))
]));
a.appendChild(scard);
// Enrollments
const mine = state.enrollments.filter(en=>{
const s = state.students.find(st=>st.id===en.studentId);
return s && s.clientId===client.id;
});
const ecard = el('div',{class:'card'},[ el('h2',{},'My enrollments') ]);
if(mine.length===0) ecard.appendChild(el('div',{},'No active enrollments.'));
else{
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Student'), el('th',{},'Plan'), el('th',{},'Billing'), el('th',{},'Payment'), el('th',{},'Status') ]) ]);
mine.forEach(en=>{
const stu = state.students.find(s=>s.id===en.studentId);
const plan = CATALOG.find(p=>p.code===en.planCode);
t.appendChild(el('tr',{},[ el('td',{}, stu?stu.name:'?'), el('td',{}, plan?plan.name:'?'), el('td',{}, en.billing), el('td',{}, en.payment||'-'), el('td',{}, en.status) ]));
});
ecard.appendChild(t);
}
a.appendChild(ecard);
});
// ENROLL
route('enroll', () => {
const u = currentUser(); if(!u||u.role!=='client'){ go('home'); return; }
setNav([{label:'My dashboard',path:'client'},{label:'Enroll',path:'enroll'},{label:'Logout',path:'logout'}]);
const a = clear();
const client = state.clients.find(c=>c.userId===u.id);
const studs = state.students.filter(s=>s.clientId===client.id);
if(studs.length===0){ a.appendChild(el('div',{class:'card'},'Add a student first on the dashboard.')); return; }
const billingSel = el('select',{class:'input',id:'bill'}); ['monthly','term1','term2','term3','full'].forEach(b=>billingSel.appendChild(el('option',{value:b},b)));
const paySel = el('select',{class:'input',id:'pay'}); ['Bank transfer','Cash'].forEach(m=>paySel.appendChild(el('option',{value:m},m)));
const stuSel = el('select',{class:'input',id:'stu'}); studs.forEach(s=>stuSel.appendChild(el('option',{value:s.id},s.name)));
const planSel = el('select',{class:'input',id:'plan'});
CATALOG.forEach(p => planSel.appendChild(el('option',{value:p.code}, `${p.name} — €${p.monthly}/month`)));
a.appendChild(el('div',{class:'card'},[
el('h2',{},'Enroll a student'),
el('div',{class:'row'},[
el('div',{class:'col'},[ el('div',{},'Student'), stuSel ]),
el('div',{class:'col'},[ el('div',{},'Plan'), planSel ]),
el('div',{class:'col'},[ el('div',{},'Billing cycle'), billingSel ]),
el('div',{class:'col'},[ el('div',{},'Payment method'), paySel ]),
]),
el('div',{}, el('button',{class:'btn primary', onClick:()=>{
const en = { id:uid('en'), studentId:$('#stu').value, planCode:$('#plan').value, billing:$('#bill').value, payment:$('#pay').value, status:'active', createdAt:new Date().toISOString() };
state.enrollments.push(en); save(); alert('Enrollment created'); go('client');
}},'Create enrollment'))
]));
// Price table
const pcard = el('div',{class:'card'},[ el('h2',{},'Price reference') ]);
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Plan'), el('th',{},'Monthly'), el('th',{},'Term 1'), el('th',{},'Term 2'), el('th',{},'Term 3'), el('th',{},'Full course') ]) ]);
CATALOG.forEach(p => t.appendChild(el('tr',{},[ el('td',{},p.name), el('td',{},'€'+p.monthly), el('td',{},'€'+p.term1), el('td',{},'€'+p.term2), el('td',{},'€'+p.term3), el('td',{},'€'+p.full) ])));
pcard.appendChild(t); a.appendChild(pcard);
});
// ADMIN
route('admin', () => {
const u = currentUser(); if(!u||u.role!=='admin'){ go('home'); return; }
setNav([{label:'Dashboard',path:'admin'},{label:'Clients',path:'a_clients'},{label:'Classes',path:'a_classes'},{label:'Analysis',path:'a_analysis'},{label:'Logout',path:'logout'}]);
const a = clear();
const totals = calcRevenue();
a.appendChild(el('div',{class:'row'},[
el('div',{class:'card col'},[ el('h1',{},'Snapshot'),
el('div',{},`Active enrollments: ${state.enrollments.filter(e=>e.status==='active').length}`),
el('div',{},`Students: ${state.students.length}`),
el('div',{},`Clients: ${state.clients.length}`),
el('div',{},`Projected monthly revenue (plans): €${totals.totalMonthly.toFixed(2)}`),
el('div',{},`Matriculation due: €${totals.matriculationDue.toFixed(2)}`)
]),
el('div',{class:'card col'},[ el('h1',{},'Quick links'),
el('div',{},[ el('a',{href:'#a_clients',class:'btn'},'Manage clients'), ' ', el('a',{href:'#a_classes',class:'btn'},'Manage classes'), ' ', el('a',{href:'#a_analysis',class:'btn'},'Open analysis') ])
])
]));
});
route('a_clients', () => {
const u = currentUser(); if(!u||u.role!=='admin'){ go('home'); return; }
setNav([{label:'Dashboard',path:'admin'},{label:'Clients',path:'a_clients'},{label:'Classes',path:'a_classes'},{label:'Analysis',path:'a_analysis'},{label:'Logout',path:'logout'}]);
const a = clear();
const card = el('div',{class:'card'},[ el('h1',{},'Clients') ]);
if(state.clients.length===0) card.appendChild(el('div',{},'No clients yet.'));
else{
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Name'), el('th',{},'Email'), el('th',{},'Phone'), el('th',{},'Returning?'), el('th',{},'Actions') ]) ]);
state.clients.forEach(c => {
t.appendChild(el('tr',{},[ el('td',{},c.name), el('td',{},c.email), el('td',{},c.phone||'-'), el('td',{}, c.isReturning?'Yes':'No'), el('td',{}, el('a',{href:'#cli_'+c.id,class:'btn'},'Open')) ]));
});
card.appendChild(t);
}
a.appendChild(card);
});
routes['cli_*'] = (id) => {
const u = currentUser(); if(!u||u.role!=='admin'){ go('home'); return; }
setNav([{label:'Back to clients',path:'a_clients'},{label:'Classes',path:'a_classes'},{label:'Analysis',path:'a_analysis'},{label:'Logout',path:'logout'}]);
const a = clear();
const c = state.clients.find(x=>x.id===id);
if(!c){ a.appendChild(el('div',{class:'card'},'Client not found')); return; }
a.appendChild(el('div',{class:'card'},[ el('h1',{},'Client'),
el('div',{},`Name: ${c.name}`), el('div',{},`Email: ${c.email}`), el('div',{},`Phone: ${c.phone||'-'}`),
el('div',{},`Returning: ${c.isReturning?'Yes':'No'}`), el('div',{},`Matriculation paid: ${c.matriculationPaid?'Yes':'No'}`),
el('div',{},[ el('button',{class:'btn',onClick:()=>{ c.isReturning=!c.isReturning; save(); routes['cli_*'](id); }}, c.isReturning?'Mark as new':'Mark as returning'), ' ', el('button',{class:'btn',onClick:()=>{ c.matriculationPaid=true; save(); routes['cli_*'](id); }}, 'Mark matriculation paid') ])
]));
const studs = state.students.filter(s=>s.clientId===c.id);
const sCard = el('div',{class:'card'},[ el('h2',{},'Students') ]);
if(studs.length===0) sCard.appendChild(el('div',{},'No students'));
else{
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Name'), el('th',{},'Enrollments') ]) ]);
studs.forEach(s=>{
const cnt = state.enrollments.filter(e=>e.studentId===s.id).length;
t.appendChild(el('tr',{},[ el('td',{}, s.name + (s.isClientSelf?' (client)':'')), el('td',{}, String(cnt)) ]));
});
sCard.appendChild(t);
}
a.appendChild(sCard);
const ens = state.enrollments.filter(e=>{
const s = state.students.find(st=>st.id===e.studentId);
return s && s.clientId===c.id;
});
const eCard = el('div',{class:'card'},[ el('h2',{},'Enrollments') ]);
if(ens.length===0) eCard.appendChild(el('div',{},'None'));
else{
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Student'), el('th',{},'Plan'), el('th',{},'Billing'), el('th',{},'Payment'), el('th',{},'Status') ]) ]);
ens.forEach(en=>{
const s = state.students.find(st=>st.id===en.studentId);
const plan = CATALOG.find(p=>p.code===en.planCode);
t.appendChild(el('tr',{},[ el('td',{}, s?s.name:'?'), el('td',{}, plan?plan.name:'?'), el('td',{}, en.billing), el('td',{}, en.payment||'-'), el('td',{}, en.status) ]));
});
eCard.appendChild(t);
}
a.appendChild(eCard);
};
route('a_classes', () => {
const u = currentUser(); if(!u||u.role!=='admin'){ go('home'); return; }
setNav([{label:'Dashboard',path:'admin'},{label:'Clients',path:'a_clients'},{label:'Classes',path:'a_classes'},{label:'Analysis',path:'a_analysis'},{label:'Logout',path:'logout'}]);
const a = clear();
// treat each non-combo plan as a "class" for roster & attendance
const classes = CATALOG.filter(p => !p.combo);
const card = el('div',{class:'card'},[ el('h1',{},'Classes') ]);
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Class (plan)'), el('th',{},'Monthly €'), el('th',{},'Actions') ]) ]);
classes.forEach(cls=>{
t.appendChild(el('tr',{},[ el('td',{},cls.name), el('td',{},'€'+cls.monthly), el('td',{}, el('a',{href:'#cls_'+cls.code,class:'btn'},'Open')) ]));
});
card.appendChild(t); a.appendChild(card);
});
routes['cls_*'] = (code) => {
const u = currentUser(); if(!u||u.role!=='admin'){ go('home'); return; }
setNav([{label:'Back to classes',path:'a_classes'},{label:'Clients',path:'a_clients'},{label:'Analysis',path:'a_analysis'},{label:'Logout',path:'logout'}]);
const a = clear();
const cls = CATALOG.find(p=>p.code===code); if(!cls){ a.appendChild(el('div',{class:'card'},'Class not found')); return; }
const roster = state.enrollments.filter(e=>e.planCode===code && e.status==='active').map(e=>({en:e, stu: state.students.find(s=>s.id===e.studentId), cli: (()=>{ const st = state.students.find(s=>s.id===e.studentId); return st? state.clients.find(c=>c.id===st.clientId) : null; })()}));
a.appendChild(el('div',{class:'card'},[ el('h1',{},'Class'), el('div',{},`Name: ${cls.name}`), el('div',{},`Monthly price: €${cls.monthly}`) ]));
const r = el('div',{class:'card'},[ el('h2',{},'Roster') ]);
if(roster.length===0) r.appendChild(el('div',{},'No active enrollments.'));
else{
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Student'), el('th',{},'Client'), el('th',{},'Payment'), el('th',{},'Billing') ]) ]);
roster.forEach(row=> t.appendChild(el('tr',{},[ el('td',{}, row.stu?row.stu.name:'?'), el('td',{}, row.cli?row.cli.name:'?'), el('td',{}, row.en.payment||'-'), el('td',{}, row.en.billing) ])));
r.appendChild(t);
}
a.appendChild(r);
// Attendance
const acard = el('div',{class:'card'},[ el('h2',{},'Attendance') ]);
const dateIn = el('input',{class:'input',type:'date',value:today(),id:'att_date'});
acard.appendChild(el('div',{},'Select date:'));
acard.appendChild(dateIn);
if(roster.length>0){
acard.appendChild(el('div',{class:'small'},'Click names to toggle present/absent. (Combos are not listed here.)'));
const list = el('div',{});
roster.forEach(row=>{
const rec = state.attendance.find(a=>a.planCode===code && a.dateISO===$('#att_date').value && a.studentId===row.stu.id);
const present = rec ? rec.status==='present' : false;
list.appendChild(el('button',{class:'btn'+(present?' primary':''), onClick:()=>{
let r = state.attendance.find(a=>a.planCode===code && a.dateISO===$('#att_date').value && a.studentId===row.stu.id);
if(!r){ r = {id:uid('att'), planCode:code, dateISO:$('#att_date').value, studentId:row.stu.id, status:'present'}; state.attendance.push(r); }
else { r.status = r.status==='present' ? 'absent' : 'present'; }
save(); routes['cls_*'](code);
}}, (row.stu?row.stu.name:'?') + ' • ' + (present?'Present':'Absent')));
});
acard.appendChild(list);
}
a.appendChild(acard);
};
// ANALYSIS
route('a_analysis', () => {
const u = currentUser(); if(!u||u.role!=='admin'){ go('home'); return; }
setNav([{label:'Dashboard',path:'admin'},{label:'Clients',path:'a_clients'},{label:'Classes',path:'a_classes'},{label:'Analysis',path:'a_analysis'},{label:'Logout',path:'logout'}]);
const a = clear();
const totals = calcRevenue();
const byClass = el('div',{class:'card'},[ el('h1',{},'Revenue by class (monthly)') ]);
const t = el('table',{class:'table'},[ el('tr',{},[ el('th',{},'Plan'), el('th',{},'Active students'), el('th',{},'Monthly €') ]) ]);
totals.byClass.forEach(r => t.appendChild(el('tr',{},[ el('td',{},r.name), el('td',{},String(r.count)), el('td',{}, r.total.toFixed(2)) ])));
byClass.appendChild(t);
a.appendChild(byClass);
const mc = el('div',{class:'card'},[ el('h2',{},'Matriculation summary') ]);
mc.appendChild(el('div',{}, 'Clients needing matriculation: ' + totals.matriculationCount));
mc.appendChild(el('div',{}, 'Matriculation due total: €' + totals.matriculationDue.toFixed(2)));
a.appendChild(mc);
});
route('logout', ()=>{ state.auth.currentUserId=null; save(); go('home'); });
// Router dispatcher
window.addEventListener('hashchange', () => {
const h = location.hash.replace('#','');
if(routes[h]) return routes[h]();
// dynamic
for(const k of Object.keys(routes)){ if(!k.includes('*')) continue; const pre = k.split('*')[0]; if(h.startsWith(pre)){ const id = h.slice(pre.length); return routes[k](id); } }
routes['home']();
});
// Revenue calc (by class plan only; combos listed as their own rows)
function calcRevenue(){
const monthlyTotals = CATALOG.map(p => {
const active = state.enrollments.filter(e => e.planCode===p.code && e.status==='active');
const total = active.reduce((sum,_)=>sum + p.monthly, 0);
return { code:p.code, name:p.name, count:active.length, total };
});
const totalMonthly = monthlyTotals.reduce((s,r)=>s+r.total,0);
const needMat = state.clients.filter(c => !c.isReturning && !c.matriculationPaid).filter(c => {
const stus = state.students.filter(s=>s.clientId===c.id).map(s=>s.id);
return state.enrollments.some(e=>stus.includes(e.studentId) && e.status==='active');
});
const due = needMat.length * state.settings.matriculationFee;
return { byClass: monthlyTotals, totalMonthly, matriculationCount: needMat.length, matriculationDue: due };
}
// Start
routes['home']();
</script>
Our Clients














