/* ============================================================================ MOWDEW — Homeowner screens, part 2 h_ledger (payment STATEMENT · distinct artifact: year control, filters, ledger-style entries) · h_summary (monthly recap) ========================================================================= */ /* ============================== PAYMENT LEDGER / STATEMENT ============= */ function HomeLedger({ go, openFab }) { const [q, setQ] = React.useState(''); const [filter, setFilter] = React.useState('all'); // all | paid | due const [year, setYear] = React.useState(2026); const paidYTD = LEDGER.filter(r => r.status === 'paid').reduce((s, r) => s + r.cents, 0); const due = LEDGER.filter(r => r.status === 'unpaid').reduce((s, r) => s + r.cents, 0); const billed = paidYTD + due; const paidCount = LEDGER.filter(r => r.status === 'paid').length; const paidVisits = LEDGER.filter(r => r.status === 'paid').reduce((s, r) => s + r.visits, 0); const counts = { all: LEDGER.length, paid: paidCount, due: LEDGER.length - paidCount }; const rows = LEDGER .filter(r => filter === 'all' || (filter === 'paid' ? r.status === 'paid' : r.status === 'unpaid')) .filter(r => r.month.toLowerCase().includes(q.toLowerCase())); const YearBtn = ({ dir, label }) => ( ); return ( }> {/* statement header + year control */}
PAYMENT STATEMENT
Your ledger
{year}
{/* statement summary band — distinct dark artifact */}
CLEARED · {year}
{fmtMoney(paidYTD)}
{paidCount} months · {paidVisits} visits
due && go('h_inbox')} style={{ flex: 1, paddingLeft: 16, borderLeft: '1px solid rgba(255,255,255,.16)', cursor: due ? 'pointer' : 'default' }}>
STILL DUE
{fmtMoney(due)}
{counts.due} invoice{counts.due === 1 ? '' : 's'} open
{/* collected vs billed */}
{Math.round((paidYTD / billed) * 100)}% CLEAREDBILLED {fmtMoney(billed)}
{/* controls: filter + search */}
{[['all', 'All'], ['paid', 'Cleared'], ['due', 'Due']].map(([id, lb]) => { const on = filter === id; return ( ); })}
setQ(e.target.value)} className="input" placeholder="Find a month — e.g. March" style={{ paddingLeft: 42 }} />
{/* ledger entries */}
{rows.map(r => { const unpaid = r.status === 'unpaid'; const [mo, yr] = r.month.split(' '); return (
go(unpaid ? 'h_inbox' : 'h_summary', { id: r.id })} className="card" style={{ padding: '12px 13px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 12 }}> {/* date column */}
{mo.slice(0, 3)}
{yr}
{r.visits} visits {unpaid ? Due : Cleared}
{unpaid ? {r.invoice} : <>Paid {r.paidOn} · {RAIL[r.rail].label}}
{unpaid ? 'UNPAID' : 'CLEARED'}
); })} {rows.length === 0 && (
{q ? `No months match "${q}".` : 'Nothing here for this filter.'}
)}
Cleared months lock — Mowdew won't let you pay one twice.
); } /* ============================== MONTHLY SUMMARY ======================= */ function HomeSummary({ go, params }) { const r = LEDGER.find(x => x.id === params.id) || LEDGER[1]; const visitDates = ['Apr 28', 'Apr 21', 'Apr 14', 'Apr 7']; return ( }> go('h_ledger')} /> {/* hero recap card */}
Month, on the record
{r.month.split(' ')[0]}: {r.visits} visits, {fmtMoney(r.cents).replace('.00', '')}, all paid.
{[['Visits', r.visits], ['Total', fmtMoney(r.cents).replace('.00', '')], ['Avg', fmtMoney(Math.round(r.cents / r.visits)).replace('.00', '')]].map(([k, v], i) => (
{v}
{k}
))}
{/* paid via */}
Paid in full
{r.paidOn} · {r.invoice}
{RAIL[r.rail].label}
{/* visit list */}
The {r.visits} verified visits
{visitDates.slice(0, r.visits).map((d, i) => (
go('h_visit', { id: 'v2' })} style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '11px 14px', borderTop: i ? '1px solid var(--ink-100)' : 'none', cursor: 'pointer' }}>
Pool service
{d} · Verified
))}
); } Object.assign(window, { HomeLedger, HomeSummary });