// screens.jsx — Dashboard, Budget, Providers const { useState: useS2 } = React; // ── Greeting header used across screens ───────────────────────────── function PageHead({ title, sub, right }) { return (

{title}

{sub &&

{sub}

}
{right}
); } // ══════════════════════════════════════════════════════════════════ // DASHBOARD // ══════════════════════════════════════════════════════════════════ function Dashboard({ go }) { const S = useHaven(); const plan = S.plan; const hr = TODAY.getHours(); const greet = hr < 12 ? 'Good morning' : hr < 18 ? 'Good afternoon' : 'Good evening'; const remaining = useCountUp(plan.remaining); const pacing = plan.pct - PLAN_TIME_PCT; // + = ahead of pace (spending faster) const quick = [ { label: 'Log a claim', icon: 'receipt', variant: 'primary', go: () => go('claims', 'new') }, { label: 'Add an invoice', icon: 'upload', variant: 'white', go: () => go('invoices', 'add') }, { label: 'Pay a worker', icon: 'people', variant: 'white', go: () => go('workers') }, { label: 'Review prep', icon: 'sparkles', variant: 'white', go: () => go('review') }, ]; return (
{greet}, {S.doc.child.parent || 'there'}.} sub={Here's where {S.doc.child.name}'s plan stands today, {fmtDate(TODAY)}. A few small things need you — nothing urgent.} /> {/* quick actions */}
{quick.map((q) => ( ))}
{/* money at a glance */}
remaining {fmtMoney(remaining)} of {fmtMoney(plan.total)} {PLAN_DAYS_LEFT} days left in plan

Money at a glance

{S.doc.budget.map((c) => { const over = c.pct > PLAN_TIME_PCT + 0.06; return (
{c.name} {over && {Math.round(c.pct * 100)}% used} {fmtMoney(c.remaining)} left
); })}
0.05 ? 'amber' : 'good'} icon={pacing > 0.05 ? 'info' : 'checkCircle'}> {pacing > 0.05 ? <>You're spending a little faster than the calendar — you've used {Math.round(plan.pct * 100)}% of funds and {Math.round(PLAN_TIME_PCT * 100)}% of the plan year has passed. The marker line shows where you'd be "on pace". Worth a glance, not a worry. : <>You're tracking nicely — spending is roughly in step with how much of the plan year has gone by.}
{/* two columns: needs you + activity */}
Needs you
{S.reminders.map((r) => )}
Recent activity {S.doc.activity.length === 0 &&
Nothing yet — what you do in Haven will show up here.
} {S.doc.activity.map((a, i, arr) => { const tone = { paid: 'var(--good)', claim: 'var(--primary)', invoice: 'var(--amber-ink)', pay: 'var(--core)' }[a.type]; const ic = { paid: 'checkCircle', claim: 'send', invoice: 'receipt', pay: 'people' }[a.type]; const I = Icons[ic]; return (
{a.text}
{fmtDateShort(a.date)}
{a.type === 'paid' ? '+' : ''}{fmtMoney(a.amount, 2)}
); })}
); } function ReminderCard({ r, go }) { const tones = { amber: { bar: 'var(--amber)', ic: 'var(--amber-ink)', icbg: 'var(--amber-soft)' }, alert: { bar: 'var(--alert)', ic: 'var(--alert)', icbg: 'var(--alert-soft)' }, primary: { bar: 'var(--primary)', ic: 'var(--primary)', icbg: 'var(--primary-soft)' }, }[r.tone]; const I = Icons[r.icon]; return ( go(r.to)} style={{ overflow: 'hidden', display: 'flex' }}>
{r.title}
{r.body}
{r.cta}
); } // ══════════════════════════════════════════════════════════════════ // BUDGET // ══════════════════════════════════════════════════════════════════ // month-by-month spending: claims (not bounced) + paid worker hours function monthlySpend(S) { const start = new Date(PLAN_START); const end = TODAY < PLAN_END ? TODAY : PLAN_END; const months = []; const d = new Date(start.getFullYear(), start.getMonth(), 1); while (d <= end) { months.push({ key: d.getFullYear() + '-' + (d.getMonth() + 1), label: d.toLocaleDateString('en-AU', { month: 'short' }), perCat: {}, total: 0 }); d.setMonth(d.getMonth() + 1); } const idx = {}; months.forEach((m) => { idx[m.key] = m; }); const add = (dateStr, cat, amt) => { const dd = new Date(dateStr); const m = idx[dd.getFullYear() + '-' + (dd.getMonth() + 1)]; if (m && amt > 0) { m.perCat[cat] = (m.perCat[cat] || 0) + amt; m.total += amt; } }; S.doc.claims.forEach((c) => { if (c.status !== 'rejected') add(c.date, c.cat, c.amount); }); S.doc.timesheets.forEach((t) => { if (t.status === 'paid') { const w = S.doc.workers.find((x) => x.id === t.worker); if (w) add(t.date, 'core', t.hours * w.rate); } }); return months; } function SpendChart({ S }) { const months = monthlySpend(S); const maxT = Math.max.apply(null, months.map((m) => m.total).concat([1])); const hasAny = months.some((m) => m.total > 0); return ( {!hasAny &&
No spending recorded yet — the chart fills in as you log claims and pay workers.
} {hasAny && (
{months.map((m) => (
{m.total > 0 &&
{m.total >= 1000 ? '$' + (m.total / 1000).toFixed(1) + 'k' : fmtMoney(m.total)}
} {S.doc.budget.map((c) => { const v = m.perCat[c.key] || 0; if (!v) return null; return
; })}
))}
{months.map((m) =>
{m.label}
)}
{S.doc.budget.map((c) => ( {c.name} ))}
)}
); } function Budget() { const S = useHaven(); const plan = S.plan; const [open, setOpen] = useS2('capacity'); return (
How {S.doc.child.name}'s funding is split, what's left, and whether you're on pace. The faint line on each bar is where you'd be if you spent evenly across the year.} /> {/* summary strip */}
{[ { label: 'Total funded', val: fmtMoney(plan.total), foot: fmtDate(PLAN_START) + ' – ' + fmtDate(PLAN_END) }, { label: 'Spent so far', val: fmtMoney(plan.spent), foot: Math.round(plan.pct * 100) + '% of plan' }, { label: 'Remaining', val: fmtMoney(plan.remaining), foot: 'across all categories', accent: true }, { label: 'Days left', val: PLAN_DAYS_LEFT, foot: Math.round((1 - PLAN_TIME_PCT) * 100) + '% of year to go' }, ].map((s) => (
{s.label}
{s.val}
{s.foot}
))}
Spending over time
{S.doc.budget.map((c) => { const isOpen = open === c.key; const over = c.pct > PLAN_TIME_PCT + 0.06; return (
setOpen(isOpen ? null : c.key)} style={{ display: 'flex', alignItems: 'center', gap: 18, padding: '20px 24px', cursor: 'pointer' }}> {Math.round(c.pct * 100)}%

{over && Spending ahead of pace}
{fmtMoney(c.remaining)}
left of {fmtMoney(c.budget)}
{isOpen && (
{c.plain}
{c.subs.map((s) => (
{s.name}
{fmtMoney(s.spent)} / {fmtMoney(s.budget)}
))}
)}
); })}
); } // ══════════════════════════════════════════════════════════════════ // PROVIDERS // ══════════════════════════════════════════════════════════════════ function Providers() { const S = useHaven(); return (
Everyone you buy supports from, their rates, and when each needs renewing.} /> {S.doc.providers.length === 0 && No providers yet — add your therapists, programs and suppliers in Settings and they'll appear here and in the claim form.}
{S.doc.providers.map((p) => { const days = p.agreementEnds ? daysBetween(TODAY, new Date(p.agreementEnds)) : null; const soon = days != null && days <= 21; const cat = S.doc.budget.find((b) => b.key === p.cat); return (
w[0]).slice(0, 2).join('')} bg={cat.soft} color={cat.color} size={44} /> {p.agreementEnds ? {soon ? : }{soon ? `Ends in ${days}d` : 'Agreement active'} : No agreement}

{p.name}

{p.sub}
Rate {p.rate} {p.agreementEnds && Renews {fmtDateShort(p.agreementEnds)}}
{p.agreementFile && p.agreementFile.url ? ( Open the agreement ) : ( )}
); })}
); } Object.assign(window, { Dashboard, Budget, Providers, PageHead, ReminderCard });