`${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();
}