// settings.jsx — where Haven becomes yours: real plan data, edited in place const { useState: uSS } = React; const sInput = { width: '100%', padding: '10px 12px', fontSize: 14, fontFamily: 'var(--sans)', border: '1px solid var(--line-2)', borderRadius: 10, background: 'var(--surface)', color: 'var(--ink)', outline: 'none', boxSizing: 'border-box' }; const sLabel = { fontSize: 11.5, fontWeight: 700, color: 'var(--ink-3)', marginBottom: 5, display: 'block', textTransform: 'uppercase', letterSpacing: '.05em' }; const num = (v) => (v === '' || v == null ? 0 : (parseFloat(v) || 0)); // input with a focus buffer: you can type freely (incl. decimals); // every keystroke still saves, display never fights your typing function SField({ label, value, onChange, type = 'text', placeholder, prefix }) { const [local, setLocal] = uSS(null); const shown = local != null ? local : (value == null ? '' : value); return (
{label && }
{prefix && {prefix}} setLocal(String(value == null ? '' : value))} onChange={(e) => { setLocal(e.target.value); onChange(e.target.value); }} onBlur={() => setLocal(null)} style={{ ...sInput, paddingLeft: prefix ? 26 : 12 }} />
); } function TrashBtn({ onClick, title = 'Remove' }) { const [h, setH] = uSS(false); return ( ); } function AddRowBtn({ onClick, children }) { return ; } function Settings() { const S = useHaven(); const d = S.doc; const [confirming, setConfirming] = uSS(false); const [restorePreview, setRestorePreview] = uSS(null); // { doc } or { error: true } const pickPlanDoc = async (e) => { const fl = e.target.files && e.target.files[0]; e.target.value = ''; if (!fl) return; try { const r = await Store.uploadFile(fl); S.setPlanDoc({ name: r.name, url: r.url }); } catch (err) { alert("Couldn't store the file — is the NAS reachable?"); } }; const pickBackup = (e) => { const file = e.target.files && e.target.files[0]; e.target.value = ''; if (!file) return; const r = new FileReader(); r.onload = () => { try { const doc = JSON.parse(r.result); if (!doc || !doc.version || !doc.budget || !doc.child) throw new Error('not haven'); setRestorePreview({ doc }); } catch (err) { setRestorePreview({ error: true }); } }; r.readAsText(file); }; return (
{/* ── child & plan ── */} The plan
S.setChild({ name: v })} /> S.setChild({ ndis: v })} /> S.setChild({ parent: v })} /> v && S.setPlanPeriod({ start: v })} /> v && S.setPlanPeriod({ end: v })} />
{d.planDoc && d.planDoc.url && ( Open the plan document )}
The plan dates drive the "on pace" marker, the days-remaining countdown and the review reminder — worth getting exactly right.
{/* ── budget ── */} The amounts from the plan, per support area. "Spent so far" is for money already used before you started Haven — it keeps your remaining figures honest.}>Budget
{d.budget.map((c) => (
{c.name} {fmtMoney(c.budget)} total
Support areaYearly budgetSpent so far
{c.subs.map((s, i) => (
S.subPatch(c.key, i, { name: v })} /> S.subPatch(c.key, i, { budget: num(v) })} /> S.subPatch(c.key, i, { spent: num(v) })} /> S.subRemove(c.key, i)} />
))}
S.subAdd(c.key)}>Add a support area
))}
{/* ── providers ── */} S.listAdd('providers', { name: '', short: '', cat: 'core', sub: '', rate: '', agreementEnds: null })}>Add provider}>Providers
{d.providers.length === 0 && No providers yet — add your therapists, programs and suppliers so logging a claim is one click.} {d.providers.map((p) => (
S.listPatch('providers', p.id, { name: v, short: v.split(' ').slice(0, 3).join(' ') })} />
S.listRemove('providers', p.id)} />
S.listPatch('providers', p.id, { sub: v })} /> S.listPatch('providers', p.id, { rate: v })} /> S.listPatch('providers', p.id, { agreementEnds: v || null })} />
))}
{/* ── workers ── */} S.listAdd('workers', { name: '', role: 'Support Worker', rate: 0, initials: '', cat: 'core' })}>Add worker}>Support workers {d.workers.length === 0 &&
No support workers yet.
} {d.workers.length > 0 && (
NameRoleHourly rate
)}
{d.workers.map((w) => (
S.listPatch('workers', w.id, { name: v, initials: v.split(' ').map((x) => x[0] || '').join('').slice(0, 2).toUpperCase() })} /> S.listPatch('workers', w.id, { role: v })} /> S.listPatch('workers', w.id, { rate: num(v) })} /> S.listRemove('workers', w.id)} />
))}
{/* ── goals ── */} S.listAdd('goals', { text: '', supports: '', progress: 0, note: '' })}>Add goal}>Goals
{d.goals.length === 0 && No goals yet — copy them in from the plan document.} {d.goals.map((g) => (
S.listPatch('goals', g.id, { text: v })} /> S.listPatch('goals', g.id, { progress: Math.min(100, Math.max(0, num(v))) / 100 })} /> S.listRemove('goals', g.id)} />
S.listPatch('goals', g.id, { supports: v })} /> S.listPatch('goals', g.id, { note: v })} />
))}
{/* ── backup & restore ── */} Backup & restore
The backup file holds every record — budgets, claims, invoices, workers, goals and notes. Your uploaded invoice files live separately in web/haven/api/files on the NAS; include that folder in your Hyper Backup.
{/* ── start fresh ── */} Start fresh
Clears every sample claim, invoice, timesheet, provider, worker and goal, and zeroes the budgets — leaving a clean Haven ready for the real plan. {d.child.name ? `${d.child.name}'s` : 'The'} name and plan dates stay.
setConfirming(false)} width={480} title="Start fresh?" sub="A clean slate for real records.">
This removes all records currently in Haven on every computer. Your NAS keeps a daily snapshot in api/store/backups, so yesterday's data is still recoverable if you change your mind.
{/* ── restore preview ── */} setRestorePreview(null)} width={480} title={restorePreview && restorePreview.error ? "That's not a Haven backup" : 'Restore this backup?'} sub={restorePreview && restorePreview.error ? null : 'Check it\u2019s the right one before anything changes.'}> {restorePreview && restorePreview.error ? (
That file couldn't be read as a Haven backup. Look for a file named like haven-backup-2026-06-10.json.
) : restorePreview && (
{[ ['For', `${restorePreview.doc.child.name || '—'} · NDIS ${restorePreview.doc.child.ndis || '—'}`], ['Last saved', restorePreview.doc.updatedAt ? new Date(restorePreview.doc.updatedAt).toLocaleString('en-AU') : '—'], ['Claims', (restorePreview.doc.claims || []).length], ['Invoices', (restorePreview.doc.invoices || []).length], ['Providers · workers · goals', `${(restorePreview.doc.providers || []).length} · ${(restorePreview.doc.workers || []).length} · ${(restorePreview.doc.goals || []).length}`], ].map(([k, v]) => (
{k}{v}
))}
Restoring replaces everything currently in Haven, on every computer. Consider downloading a backup of the current data first.
)}
); } Object.assign(window, { Settings });