// ══════════════════════════════════════════════ // GOVERNANCE HUB — Supabase-backed (replaces localStorage govStore) // ══════════════════════════════════════════════ let hubData = { policies: [], kpis: [], audits: [], actionPlans: [], complaints: [], compliments: [], medicationAudits: [], supervisions: [], risks: [], safeguarding: [], meetings: [] }; async function loadHubData() { const [policies, kpis, audits, actionPlans, complaints, compliments, medicationAudits, supervisions, risks, safeguarding, meetings] = await Promise.all([ db.from('hub_policies').select('*').order('created_at', { ascending: false }), db.from('hub_kpis').select('*').order('created_at', { ascending: false }), db.from('hub_audits').select('*').order('audit_date', { ascending: false }), db.from('hub_action_plans').select('*').order('created_at', { ascending: false }), db.from('hub_complaints').select('*').order('date_received', { ascending: false }), db.from('hub_compliments').select('*').order('date_received', { ascending: false }), db.from('hub_medication_audits').select('*').order('audit_date', { ascending: false }), db.from('hub_supervisions').select('*').order('supervision_date', { ascending: false }), db.from('hub_risks').select('*').order('created_at', { ascending: false }), db.from('hub_safeguarding').select('*').order('date_of_concern', { ascending: false }), db.from('hub_meetings').select('*').order('meeting_date', { ascending: false }), ]); hubData.policies = policies.data || []; hubData.kpis = kpis.data || []; hubData.audits = audits.data || []; hubData.actionPlans = actionPlans.data || []; hubData.complaints = complaints.data || []; hubData.compliments = compliments.data || []; hubData.medicationAudits = medicationAudits.data || []; hubData.supervisions = supervisions.data || []; hubData.risks = risks.data || []; hubData.safeguarding = safeguarding.data || []; hubData.meetings = meetings.data || []; } // Buckets are private, so we generate a fresh short-lived signed URL at // the moment a file is opened, rather than storing a permanent public link. async function openHubFile(path) { if (!path) { alert('No file attached to this record.'); return; } const { data, error } = await db.storage.from('documents').createSignedUrl(path, 3600); if (error) { alert('Could not open file: ' + error.message); return; } window.open(data.signedUrl, '_blank'); } function showGovSection(id, btn){ document.querySelectorAll('.gov-section').forEach(s=>s.classList.remove('active')); document.querySelectorAll('.gov-tab').forEach(b=>b.classList.remove('active')); document.getElementById('gov-'+id).classList.add('active'); btn.classList.add('active'); if(id==='overview') updateGovOverview(); } function openGovModal(id){ document.getElementById(id).classList.add('open'); } function closeGovModal(id){ document.getElementById(id).classList.remove('open'); } function govFmtDate(d){ if(!d) return '—'; return new Date(d).toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}); } function govRagBadge(r){ const map={green:'badge-green',amber:'badge-amber',red:'badge-red',blue:'badge-blue'}; return `${(r||'').charAt(0).toUpperCase()+(r||'').slice(1)}`; } function govStatusBadge(s){ const map={Open:'badge-red','Under investigation':'badge-amber','In progress':'badge-amber',Resolved:'badge-green',Completed:'badge-green',Escalated:'badge-red',Current:'badge-green','Due for review':'badge-amber',Overdue:'badge-red',Archived:'badge-blue',Satisfactory:'badge-green',Good:'badge-green',Outstanding:'badge-teal','Improvement needed':'badge-amber','Action plan set':'badge-amber'}; return `${s||'—'}`; } function govPriorityBadge(p){ const map={High:'badge-red',Medium:'badge-amber',Low:'badge-green'}; return `${p||'—'}`; } async function govDel(table, id, reloadFn) { if(!confirm('Delete this record?')) return; const { error } = await db.from(table).delete().eq('id', id); if (error) { alert('Error: ' + error.message); return; } await reloadFn(); } async function updateGovOverview(){ document.getElementById('summary-date').textContent='As of '+new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'long',year:'numeric'}); const today=new Date(); today.setHours(0,0,0,0); const polDue = hubData.policies.filter(p=>{ if(!p.next_review_due) return false; return new Date(p.next_review_due)<=new Date(today.getTime()+30*86400000); }).length; document.getElementById('ov-policies').textContent=polDue; const thisMonth=new Date().toISOString().slice(0,7); const monthKpis=hubData.kpis.filter(k=>k.month_key===thisMonth); const onTarget=monthKpis.filter(k=>parseFloat(k.actual)>=parseFloat(k.target)).length; document.getElementById('ov-kpis').textContent=monthKpis.length?`${onTarget}/${monthKpis.length}`:'—'; document.getElementById('ov-audits').textContent=hubData.audits.filter(a=>a.audit_date&&a.audit_date.startsWith(thisMonth)).length; document.getElementById('ov-actions').textContent=hubData.actionPlans.filter(a=>a.status==='Open'||a.status==='In progress').length; document.getElementById('ov-complaints').textContent=hubData.complaints.filter(c=>c.status==='Open'||c.status==='Under investigation').length; document.getElementById('ov-risks').textContent=hubData.risks.filter(r=>(parseInt(r.likelihood)*parseInt(r.impact))>=12).length; document.getElementById('ov-safeguarding').textContent=hubData.safeguarding.filter(s=>s.status==='Open'||s.status==='Under investigation').length; buildGovAlerts(); } function buildGovAlerts(){ const alerts=[]; const today=new Date(); today.setHours(0,0,0,0); const soon=new Date(today.getTime()+14*86400000); hubData.actionPlans.filter(a=>a.status!=='Completed'&&a.due_date).forEach(a=>{ const d=new Date(a.due_date); if(dc.status==='Open'&&c.response_due).forEach(c=>{ if(new Date(c.response_due)p.next_review_due).forEach(p=>{ const d=new Date(p.next_review_due); if(d`
${a.level==='red'?'Overdue':'Due soon'}${a.text}
`).join(''); } // ── Policies ── async function savePolicy(){ const name=document.getElementById('pol-name').value.trim(); if(!name){alert('Please enter a policy name');return;} let filePath = null; const file = document.getElementById('pol-file').files[0]; if (file) { filePath = `policies/${Date.now()}_${file.name.replace(/[^a-zA-Z0-9._-]/g,'_')}`; const { error: ue } = await db.storage.from('documents').upload(filePath, file, { upsert: true }); if (ue) { alert('File upload failed: ' + ue.message); return; } } const { error } = await db.from('hub_policies').insert([{ name, category: document.getElementById('pol-cat').value, version: document.getElementById('pol-ver').value, owner: document.getElementById('pol-owner').value, last_reviewed: document.getElementById('pol-reviewed').value || null, next_review_due: document.getElementById('pol-due').value || null, status: document.getElementById('pol-status').value, notes: document.getElementById('pol-notes').value, file_path: filePath }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('policy-modal'); ['pol-name','pol-ver','pol-owner','pol-reviewed','pol-due','pol-notes'].forEach(id=>document.getElementById(id).value=''); document.getElementById('pol-file').value=''; document.getElementById('pol-file-label').textContent='Click to upload policy document'; await loadHubData(); renderGovPolicies(); updateGovOverview(); } function renderGovPolicies(){ const tb=document.getElementById('policy-table'); if(!hubData.policies.length){tb.innerHTML='No policies added yet';return;} tb.innerHTML=hubData.policies.map(p=>`${p.name}${p.notes?`
${p.notes}`:''}${p.category||'—'}${p.version||'—'}${p.owner||'—'}${govFmtDate(p.last_reviewed)}${govFmtDate(p.next_review_due)}${govStatusBadge(p.status)}${p.file_path?`📎 Open`:'—'}`).join(''); } // ── KPIs ── function initGovKPIMonth(){ const sel=document.getElementById('kpi-month-select'); sel.innerHTML=''; for(let i=0;i<12;i++){ const d=new Date(); d.setMonth(d.getMonth()-i); const v=d.toISOString().slice(0,7); const opt=document.createElement('option'); opt.value=v; opt.textContent=d.toLocaleDateString('en-GB',{month:'long',year:'numeric'}); sel.appendChild(opt); } } async function saveGovKPI(){ const name=document.getElementById('kpi-name').value.trim(); if(!name){alert('Please enter a KPI name');return;} const { error } = await db.from('hub_kpis').insert([{ name, unit: document.getElementById('kpi-unit').value, target: parseFloat(document.getElementById('kpi-target').value) || null, actual: parseFloat(document.getElementById('kpi-actual').value) || null, month_key: document.getElementById('kpi-month').value || new Date().toISOString().slice(0,7), category: document.getElementById('kpi-cat').value, notes: document.getElementById('kpi-notes').value }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('kpi-modal'); ['kpi-name','kpi-target','kpi-actual','kpi-notes'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovKPIs(); updateGovOverview(); } function renderGovKPIs(){ const month=document.getElementById('kpi-month-select').value; document.getElementById('kpi-month-label').textContent=new Date(month+'-01').toLocaleDateString('en-GB',{month:'long',year:'numeric'}); const data=hubData.kpis.filter(k=>k.month_key===month); const el=document.getElementById('kpi-list'); if(!data.length){el.innerHTML='

No KPIs recorded for this month

';return;} el.innerHTML=data.map(k=>{ const t=parseFloat(k.target)||0,a=parseFloat(k.actual)||0; const pct=t>0?Math.min(Math.round(a/t*100),100):0; const col=a>=t?'#10b981':a>=t*0.9?'#f59e0b':'#ef4444'; const rag=a>=t?'green':a>=t*0.9?'amber':'red'; return `
${k.name}
${k.category||''}
${k.target ?? '—'}${k.unit||''}
${k.actual ?? '—'}${k.unit||''}
${govRagBadge(rag)}
`; }).join(''); } // ── Audits ── async function saveAudit(){ const type=document.getElementById('aud-type').value; let filePath = null; const file = document.getElementById('aud-file').files[0]; if (file) { filePath = `audits/${Date.now()}_${file.name.replace(/[^a-zA-Z0-9._-]/g,'_')}`; const { error: ue } = await db.storage.from('documents').upload(filePath, file, { upsert: true }); if (ue) { alert('File upload failed: ' + ue.message); return; } } const { error } = await db.from('hub_audits').insert([{ audit_type: type, audit_date: document.getElementById('aud-date').value || null, conducted_by: document.getElementById('aud-by').value, score: parseFloat(document.getElementById('aud-score').value) || null, rag: document.getElementById('aud-rag').value, next_audit_date: document.getElementById('aud-next').value || null, findings: document.getElementById('aud-findings').value, actions_required: document.getElementById('aud-actions-text').value, file_path: filePath }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('audit-modal'); ['aud-date','aud-by','aud-score','aud-next','aud-findings','aud-actions-text'].forEach(id=>document.getElementById(id).value=''); document.getElementById('aud-file').value=''; document.getElementById('aud-file-label').textContent='Click to upload audit document'; await loadHubData(); renderGovAudits(); updateGovOverview(); } function renderGovAudits(){ const el=document.getElementById('audit-list'); if(!hubData.audits.length){el.innerHTML='

No audits recorded yet

';return;} el.innerHTML=hubData.audits.map(a=>{ const sn=parseFloat(a.score); const bg=a.rag==='green'?'#dcfce7':a.rag==='amber'?'#fef3c7':'#fee2e2'; const tc=a.rag==='green'?'#15803d':a.rag==='amber'?'#92400e':'#b91c1c'; return `
${isNaN(sn)?'N/A':sn+'%'}
${a.audit_type}
By ${a.conducted_by||'—'} on ${govFmtDate(a.audit_date)} · Next: ${govFmtDate(a.next_audit_date)}
${a.findings?`
${a.findings}
`:''}${a.file_path?`📎 Open report`:''}
${govRagBadge(a.rag)}
`; }).join(''); } // ── Action plans ── async function saveGovAction(){ const desc=document.getElementById('act-desc').value.trim(); if(!desc){alert('Please describe the action');return;} const { error } = await db.from('hub_action_plans').insert([{ description: desc, source: document.getElementById('act-source').value, priority: document.getElementById('act-priority').value, owner: document.getElementById('act-owner').value, due_date: document.getElementById('act-due').value || null, status: document.getElementById('act-status').value, notes: document.getElementById('act-notes').value }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('action-modal'); ['act-desc','act-owner','act-due','act-notes'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovActions(); updateGovOverview(); } function renderGovActions(){ const tb=document.getElementById('action-table'); if(!hubData.actionPlans.length){tb.innerHTML='No actions recorded yet';return;} const order = {Open:0,'In progress':1,Overdue:2,Completed:3}; tb.innerHTML=[...hubData.actionPlans].sort((a,b)=>(order[a.status]||0)-(order[b.status]||0)).map(a=>`${a.description}${a.notes?`
${a.notes}`:''}${a.source||'—'}${govPriorityBadge(a.priority)}${a.owner||'—'}${govFmtDate(a.due_date)}${govStatusBadge(a.status)}`).join(''); } // ── Complaints & compliments ── async function saveComplaint(){ const date=document.getElementById('cmp-date').value; if(!date){alert('Please enter the date received');return;} const { error } = await db.from('hub_complaints').insert([{ date_received: date, raised_by: document.getElementById('cmp-raised').value, method: document.getElementById('cmp-method').value, nature: document.getElementById('cmp-nature').value, assigned_to: document.getElementById('cmp-assigned').value, response_due: document.getElementById('cmp-due').value || null, status: document.getElementById('cmp-status').value, escalated_to: document.getElementById('cmp-escalated').value, details: document.getElementById('cmp-details').value, outcome: document.getElementById('cmp-outcome').value, learning: document.getElementById('cmp-learning').value, satisfied: document.getElementById('cmp-satisfied').value }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('complaint-modal'); ['cmp-date','cmp-assigned','cmp-due','cmp-details','cmp-outcome','cmp-learning'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovComplaints(); updateGovOverview(); } async function saveCompliment(){ const date=document.getElementById('comp-date').value; if(!date){alert('Please enter the date');return;} const { error } = await db.from('hub_compliments').insert([{ date_received: date, source: document.getElementById('comp-source').value, related_to: document.getElementById('comp-re').value, staff_mentioned: document.getElementById('comp-staff').value, details: document.getElementById('comp-details').value }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('compliment-modal'); ['comp-date','comp-staff','comp-details'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovComplaints(); } function renderGovComplaints(){ const thisMonth=new Date().toISOString().slice(0,7); document.getElementById('comp-month').textContent=hubData.complaints.filter(c=>c.date_received&&c.date_received.startsWith(thisMonth)).length; document.getElementById('comp-open').textContent=hubData.complaints.filter(c=>c.status==='Open'||c.status==='Under investigation').length; document.getElementById('comp-resolved').textContent=hubData.complaints.filter(c=>c.status==='Resolved').length; document.getElementById('comp-compliments').textContent=hubData.compliments.length; const tb=document.getElementById('complaint-table'); if(!hubData.complaints.length){tb.innerHTML='No complaints logged';} else{tb.innerHTML=hubData.complaints.map(c=>`${govFmtDate(c.date_received)}${c.raised_by||'—'}${c.nature||'—'}${c.assigned_to||'—'}${govFmtDate(c.response_due)}${govStatusBadge(c.status)}${c.escalated_to?`${c.escalated_to}`:'—'}`).join('');} const tb2=document.getElementById('compliment-table'); if(!hubData.compliments.length){tb2.innerHTML='No compliments logged yet';} else{tb2.innerHTML=hubData.compliments.map(c=>`${govFmtDate(c.date_received)}${c.source||'—'}${c.related_to||'—'}${c.staff_mentioned||'—'}${c.details||'—'}`).join('');} } // ── Medication audits ── async function saveMed(){ const date=document.getElementById('med-date').value; if(!date){alert('Please enter the date');return;} const { error } = await db.from('hub_medication_audits').insert([{ audit_date: date, resident: document.getElementById('med-resident').value, auditor: document.getElementById('med-auditor').value, mar_accurate: document.getElementById('med-mar').value, stock_correct: document.getElementById('med-stock').value, errors_found: parseInt(document.getElementById('med-errors').value) || 0, findings: document.getElementById('med-findings').value, actions_required: document.getElementById('med-actions').value, score: parseFloat(document.getElementById('med-score').value) || null }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('med-modal'); ['med-date','med-resident','med-auditor','med-errors','med-findings','med-actions','med-score'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovMed(); } function renderGovMed(){ const tb=document.getElementById('med-table'); if(!hubData.medicationAudits.length){tb.innerHTML='No medication audits recorded yet';return;} tb.innerHTML=hubData.medicationAudits.map(m=>{ const err=parseInt(m.errors_found)||0; const score=parseFloat(m.score); return `${govFmtDate(m.audit_date)}${m.resident||'—'}${m.auditor||'—'}${m.mar_accurate==='Yes'?'Yes':''+m.mar_accurate+''}${m.stock_correct==='Yes'?'Yes':''+m.stock_correct+''}${err===0?'None':''+err+''}${m.actions_required||'—'}${isNaN(score)?'—':''+score+'%'}`; }).join(''); } // ── Supervisions ── async function saveSupervision(){ const staff=document.getElementById('sup-staff').value.trim(); if(!staff){alert('Please enter the staff member name');return;} const { error } = await db.from('hub_supervisions').insert([{ staff_name: staff, role: document.getElementById('sup-role').value, supervision_type: document.getElementById('sup-type').value, supervision_date: document.getElementById('sup-date').value || null, conducted_by: document.getElementById('sup-by').value, next_due: document.getElementById('sup-next').value || null, outcome: document.getElementById('sup-outcome').value, notes: document.getElementById('sup-notes').value }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('sup-modal'); ['sup-staff','sup-role','sup-date','sup-by','sup-next','sup-notes'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovSupervisions(); } function renderGovSupervisions(){ const tb=document.getElementById('sup-table'); if(!hubData.supervisions.length){tb.innerHTML='No supervisions recorded yet';return;} tb.innerHTML=hubData.supervisions.map(s=>`${s.staff_name}${s.role||'—'}${s.supervision_type||'—'}${govFmtDate(s.supervision_date)}${s.conducted_by||'—'}${govStatusBadge(s.outcome)}${govFmtDate(s.next_due)}${s.next_due && new Date(s.next_due)Overdue':'On track'}`).join(''); } // ── Risk register ── async function saveRisk(){ const desc=document.getElementById('risk-desc').value.trim(); if(!desc){alert('Please describe the risk');return;} const { error } = await db.from('hub_risks').insert([{ description: desc, category: document.getElementById('risk-cat').value, likelihood: parseInt(document.getElementById('risk-like').value), impact: parseInt(document.getElementById('risk-impact').value), owner: document.getElementById('risk-owner').value, review_date: document.getElementById('risk-review').value || null, controls_in_place: document.getElementById('risk-controls').value, further_actions: document.getElementById('risk-actions').value }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('risk-modal'); ['risk-desc','risk-owner','risk-review','risk-controls','risk-actions'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovRisks(); } function renderGovRisks(){ const tb=document.getElementById('risk-table'); if(!hubData.risks.length){tb.innerHTML='No risks recorded yet';return;} tb.innerHTML=[...hubData.risks].sort((a,b)=>((b.likelihood||0)*(b.impact||0))-((a.likelihood||0)*(a.impact||0))).map(r=>{ const score=(r.likelihood||0)*(r.impact||0); const rating=score>=15?'red':score>=8?'amber':'green'; return `${r.description}${r.category||'—'}${r.likelihood}${r.impact}${score}${score>=15?'High':score>=8?'Medium':'Low'}${r.controls_in_place||'—'}${r.owner||'—'}${govFmtDate(r.review_date)}`; }).join(''); } // ── Safeguarding ── async function saveSafeguarding(){ const date=document.getElementById('sfg-date').value; if(!date){alert('Please enter the date');return;} const { error } = await db.from('hub_safeguarding').insert([{ date_of_concern: date, resident: document.getElementById('sfg-resident').value, concern_type: document.getElementById('sfg-type').value, referred_to: document.getElementById('sfg-referred').value, referred_by: document.getElementById('sfg-by').value, status: document.getElementById('sfg-status').value, details: document.getElementById('sfg-details').value, outcome: document.getElementById('sfg-outcome').value, learning: document.getElementById('sfg-learning').value }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('sfg-modal'); ['sfg-date','sfg-resident','sfg-by','sfg-details','sfg-outcome','sfg-learning'].forEach(id=>document.getElementById(id).value=''); await loadHubData(); renderGovSafeguarding(); } function renderGovSafeguarding(){ const tb=document.getElementById('sfg-table'); if(!hubData.safeguarding.length){tb.innerHTML='No safeguarding referrals recorded';return;} tb.innerHTML=hubData.safeguarding.map(s=>`${govFmtDate(s.date_of_concern)}${s.resident||'—'}${s.concern_type||'—'}${s.referred_to||'—'}${s.referred_by||'—'}${s.outcome||'—'}${s.learning||'—'}${govStatusBadge(s.status)}`).join(''); } // ── Meeting minutes ── async function saveMeeting(){ const date=document.getElementById('mtg-date').value; if(!date){alert('Please enter the meeting date');return;} let filePath = null; const file = document.getElementById('mtg-file').files[0]; if (file) { filePath = `meetings/${Date.now()}_${file.name.replace(/[^a-zA-Z0-9._-]/g,'_')}`; const { error: ue } = await db.storage.from('documents').upload(filePath, file, { upsert: true }); if (ue) { alert('File upload failed: ' + ue.message); return; } } const { error } = await db.from('hub_meetings').insert([{ meeting_date: date, meeting_type: document.getElementById('mtg-type').value, chair: document.getElementById('mtg-chair').value, next_meeting_date: document.getElementById('mtg-next').value || null, attendees: document.getElementById('mtg-attendees').value, decisions: document.getElementById('mtg-decisions').value, actions_arising: document.getElementById('mtg-actions-text').value, notes: document.getElementById('mtg-notes').value, file_path: filePath }]); if (error) { alert('Error: ' + error.message); return; } closeGovModal('meeting-modal'); ['mtg-date','mtg-chair','mtg-next','mtg-attendees','mtg-decisions','mtg-actions-text','mtg-notes'].forEach(id=>document.getElementById(id).value=''); document.getElementById('mtg-file').value=''; document.getElementById('mtg-file-label').textContent='Click to upload meeting minutes'; await loadHubData(); renderGovMeetings(); } function renderGovMeetings(){ const tb=document.getElementById('meeting-table'); if(!hubData.meetings.length){tb.innerHTML='No meeting minutes recorded yet';return;} tb.innerHTML=hubData.meetings.map(m=>`${govFmtDate(m.meeting_date)}${m.meeting_type||'—'}${m.chair||'—'}${m.attendees||'—'}${m.decisions||'—'}${m.actions_arising||''}${m.file_path?` · 📎`:''}${govFmtDate(m.next_meeting_date)}`).join(''); } // ── Init ── async function initGovernance(){ initGovKPIMonth(); document.getElementById('kpi-month').value=new Date().toISOString().slice(0,7); await loadHubData(); updateGovOverview(); renderGovPolicies(); renderGovKPIs(); renderGovAudits(); renderGovActions(); renderGovComplaints(); renderGovMed(); renderGovSupervisions(); renderGovRisks(); renderGovSafeguarding(); renderGovMeetings(); }