/* ============================================================================ MOWDEW OWNER — Mobile app (on-the-go god mode) The owner's pocket control: see the money, nudge who owes, approve reschedules (route auto-updates), message customers, manage the book. Reuses owner data (BIZ, WORKERS, CUSTOMERS, RESCHEDULES, THREADS, PLANS). Screens: o_home · o_money · o_inbox · o_customers · o_customer · o_thread ========================================================================= */ const { useState: omState } = React; /* small vertical pill (garden green / pool aqua) */ function MVert({ vert }) { const v = VERT(vert); return ( {v.name} ); } /* rich photo-backed stat card (bespoke imagery behind the number) */ function RichStat({ scene, eyebrow, value, sub, badge, badgeTone = 'mint', onClick, tall }) { const badgeBg = { mint: 'var(--green-500)', alert: 'var(--alert-500)', sky: 'var(--blue-500)', aqua: 'var(--aqua-500)' }[badgeTone] || 'var(--green-500)'; return (
{eyebrow}
{value}
{sub}
); } /* the owner pulse — one dark statement band (money is the hero) + a calm white ops pair. Same visual language as the homeowner's ledger band. */ function OwnerStatBand({ collected, collectedSub, owed, owedSub, onOwed, visitsValue, visitsSub, visitsLive = false, onVisits, homesValue, homesSub, onHomes }) { const billed = collected + owed; const pct = Math.round((collected / billed) * 100); return (
COLLECTED · MAY
{fmtMoney(collected)}
{collectedSub}
0 ? onOwed : undefined} style={{ flex: 1, minWidth: 0, paddingLeft: 16, borderLeft: '1px solid rgba(255,255,255,.16)', cursor: owed > 0 ? 'pointer' : 'default' }}>
0 ? '#FF9E7D' : 'rgba(255,255,255,.6)' }}>OUTSTANDING{owed > 0 && }
0 ? '#FFB59A' : '#fff' }}>{fmtMoney(owed)}
{owedSub}
{pct}% COLLECTEDBILLED {fmtMoney(billed)}
{[ { icon: 'route', bg: 'var(--blue-500)', eyebrow: 'VISITS TODAY', value: visitsValue, sub: visitsSub, live: visitsLive, onClick: onVisits }, { icon: 'home', bg: 'var(--aqua-500)', eyebrow: 'ACTIVE HOMES', value: homesValue, sub: homesSub, onClick: onHomes }, ].map(c => (
{c.eyebrow} {c.live && }
{c.value}
{c.sub}
))}
); } /* the "all-clear" celebratory hero — shows when the book is fully caught up */ function AllClearHero() { return (
All clear
You're all caught up.
Everyone's paid, every route is set.
); } /* ============================== OWNER · HOME ========================== */ function OwnerHome({ go, openFab }) { const owing = CUSTOMERS.filter(c => c.balance > 0); const owed = owing.reduce((s, c) => s + c.balance, 0); const collected = 184000; const allClear = owed === 0 && RESCHEDULES.length === 0; return ( }> {/* header */}
{BIZ.name}
Good morning, Diego
go('o_notifs')} />
{/* money + ops — rich photo-backed stat cards (bespoke imagery) */}
{allClear ? : ( 0 ? `${owing.length} invoices open` : 'All collected'} onOwed={() => go('o_money')} visitsValue="9" visitsSub="2 workers out · live" visitsLive onVisits={() => go('o_routes')} homesValue={String(BIZ.homes)} homesSub="21 garden · 17 pool" onHomes={() => go('o_customers')} /> )}
{/* time to bill — owner-confirmed billing action item (hidden at zero) */} {runPending().length > 0 && (
go('o_billing_run')} />
)} {/* needs you */}
Needs you
{/* G6/G5 — field notes + photo'd requests (union: present in the app, now in the design too) */}
{/* today's routes */}
go('o_routes')} className="btn btn-ghost" style={{ minHeight: 30, padding: '5px 9px', fontSize: 12.5 }}>Dispatch & team}>Today's routes
{WORKERS.map(w => { const doneN = w.id === 'w1' ? 2 : 0; return (
go('o_routes', { worker: w.id })} className="card" style={{ padding: 13, cursor: 'pointer' }}>
{w.name}{w.you && · you}
{w.role}
{w.id === 'w1' ? 'On route' : 'Starts 11:00'}
{Array.from({ length: w.today }).map((_, i) => ( ))} {doneN}/{w.today}
); })}
); } /* ============================== OWNER · MONEY ========================= */ function OwnerMoneyM({ go, openFab, toast }) { const [nudged, setNudged] = omState({}); const owing = CUSTOMERS.filter(c => c.balance > 0); const paid = CUSTOMERS.filter(c => c.balance === 0); const owed = owing.reduce((s, c) => s + c.balance, 0); const nudge = (c) => { setNudged(n => ({ ...n, [c.id]: true })); toast(`Nudge sent to ${c.name.split(' ')[0]}`, 'green'); }; return ( }> {/* time to bill — appears whenever ≥1 customer is due (hidden at zero) */} {runPending().length > 0 && (
go('o_billing_run')} />
)} {/* summary */}
Collected · May
{fmtMoney(184000)}
Outstanding
{/* owes you */}
Owes you
{owing.map(c => (
{c.name}
{c.addr}, {c.city} · {c.dueLabel}
via {RAIL[c.rail].label}
))}
{/* paid up */}
Paid up
{paid.slice(0, 5).map((c, i) => (
{c.name}
{c.lastPaid}
Paid
))}
); } /* ============================== OWNER · INBOX ========================= */ function OwnerInbox({ go, openFab, toast, params }) { const [tab, setTab] = omState(params.tab || 'approvals'); const [resolved, setResolved] = omState({}); const act = (r, ok) => { setResolved(s => ({ ...s, [r.id]: ok ? 'approved' : 'declined' })); const w = WORKERS.find(x => x.id === r.worker); if (ok) { toast(`Approved · ${w.name.split(' ')[0]}'s route updated`, 'green'); } }; const declineMsg = (r) => { const c = CUSTOMERS.find(x => x.id === r.cust); const first = c.name.split(' ')[0]; const when = r.to === 'skip' ? 'skip this week' : `move to ${r.to}`; const pre = `Hi ${first}, thanks for the heads up! I'm not able to ${when} this time — could we keep your regular ${r.from} visit? Happy to find another day that works for you. — Diego`; setResolved(s => ({ ...s, [r.id]: 'declined' })); go('o_thread', { id: r.cust, prefill: pre }); }; const threadIds = Object.keys(THREADS); return ( }> {/* segmented */}
{[['approvals', 'Approvals', needsOwnerCount()], ['messages', 'Messages', 2]].map(([id, lb, n]) => ( ))}
{tab === 'approvals' ? (
go('o_requests')} className="card" style={{ padding: 13, display: 'flex', alignItems: 'center', gap: 11, cursor: 'pointer', borderColor: 'var(--aqua-100)', marginBottom: 12 }}>
Requests & quotes
1 photo to quote · 1 awaiting approval
) : (
{threadIds.concat(CUSTOMERS.filter(c => !threadIds.includes(c.id)).slice(0, 3).map(c => c.id)).map(id => { const c = CUSTOMERS.find(x => x.id === id); const last = (THREADS[id] || [])[(THREADS[id] || []).length - 1]; return (
go('o_thread', { id })} className="card" style={{ padding: 13, display: 'flex', alignItems: 'center', gap: 11, cursor: 'pointer' }}>
{c.name}
{last ? last.t : 'Start a conversation'}
); })}
)}
); } /* ============================== OWNER · THREAD ======================= */ function OwnerThread({ go, params }) { const c = CUSTOMERS.find(x => x.id === params.id) || CUSTOMERS[1]; const [extra, setExtra] = omState([]); const [draft, setDraft] = omState(''); const msgs = [...(THREADS[c.id] || THREADS.c2), ...extra]; const send = () => { if (!draft.trim()) return; setExtra(e => [...e, { from: 'owner', t: draft, at: 'now' }]); setDraft(''); }; return ( setDraft(e.target.value)} onKeyDown={e => e.key === 'Enter' && send()} placeholder="Write a reply…" style={{ flex: 1, padding: '12px 14px', border: '1px solid var(--ink-200)', borderRadius: 12, fontFamily: 'var(--font-body)', fontSize: 14, outline: 'none' }} />
}> go('o_inbox', { tab: 'messages' })} right={} />
{msgs.map((m, i) => { if (m.from === 'sys') return
{m.t}
; const me = m.from === 'owner'; return (
{m.t}
{m.at}
); })}
); } /* ============================== OWNER · CUSTOMERS ===================== */ function OwnerCustomersM({ go, openFab }) { const [q, setQ] = omState(''); const [vf, setVf] = omState('all'); const rows = CUSTOMERS.filter(c => (vf === 'all' || c.vert === vf) && (c.name.toLowerCase().includes(q.toLowerCase()) || c.addr.toLowerCase().includes(q.toLowerCase()))); return ( }> go('o_new_customer')} style={{ width: 42, height: 42, borderRadius: 12, border: 'none', background: 'var(--green-700)', display: 'grid', placeItems: 'center', cursor: 'pointer', color: '#fff', boxShadow: 'var(--shadow-green)' }}>} /> {/* search */}
setQ(e.target.value)} className="input" placeholder="Search name or address" style={{ paddingLeft: 44 }} />
{/* filter */}
{[['all', 'All'], ['garden', 'Garden'], ['pool', 'Pool']].map(([id, lb]) => ( ))}
{rows.map(c => { const w = custWorker(c); return (
go('o_customer', { id: c.id })} className="card" style={{ padding: 13, display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
{c.name}
{c.addr} · {w.name.split(' ')[0]}
{c.balance > 0 ? : Paid}
); })} {rows.length === 0 &&
No customers match.
}
); } /* ============================== OWNER · CUSTOMER DETAIL =============== */ function OwnerCustomerM({ go, params, toast }) { const c = CUSTOMERS.find(x => x.id === params.id) || CUSTOMERS[1]; const [wid, setWid] = omState(c.worker); const [reassign, setReassign] = omState(false); const w = WORKERS.find(x => x.id === wid) || custWorker(c); const v = VERT(c.vert); const scene = c.vert === 'pool' ? 'pool' : 'lawn'; const visits = [ { d: 'Mon, May 12', tin: '9:58 AM', tout: '10:22 AM', dur: '24 min' }, { d: 'Mon, May 5', tin: '10:04 AM', tout: '10:31 AM', dur: '27 min' }, { d: 'Mon, Apr 28', tin: '9:51 AM', tout: '10:13 AM', dur: '22 min' }, ]; const doReassign = (nw) => { setWid(nw.id); setReassign(false); toast(`${c.name.split(' ')[0]} reassigned to ${nw.name.split(' ')[0]} · routes rebalanced`, 'green'); }; return ( {c.balance > 0 ? : } }> go('o_customers')} /> {/* balance */}
0 ? 'var(--alert-50)' : 'var(--green-50)', borderColor: c.balance > 0 ? '#F8D3C5' : 'var(--green-100)', display: 'flex', alignItems: 'center', gap: 13 }}>
{c.balance > 0 ? 'Outstanding' : 'All settled'}
{c.balance > 0 ? :
$0.00
}
{/* billing history — why the balance is what it is */}
0 ? 'alert' : 'green'} icon="ledger" style={{ marginBottom: 12 }}>Billing history
{(() => { const n = c.cadence === 'Weekly' ? 4 : c.cadence === 'Monthly' ? 1 : 2; const moTotal = c.price * n; const dueMonths = c.balance > 0 ? Math.max(1, Math.round(c.balance / moTotal)) : 0; const rows = [ { m: 'May 2026', v: n, cents: moTotal, status: dueMonths >= 1 ? 'unpaid' : 'paid' }, { m: 'Apr 2026', v: n, cents: moTotal, status: dueMonths >= 2 ? 'unpaid' : 'paid', on: 'Apr 30' }, { m: 'Mar 2026', v: c.cadence === 'Weekly' ? 5 : n, cents: c.price * (c.cadence === 'Weekly' ? 5 : n), status: 'paid', on: 'Mar 28' }, ]; const runRow = BILLRUN.rows.find(rr => rr.cid === c.id); return rows.map((r, i) => { const unpaid = r.status === 'unpaid'; const linked = i === 0 && runRow; return (
linked && go('o_invoice', { inv: runRow.inv })} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '13px 14px', borderTop: i ? '1px solid var(--ink-100)' : 'none', cursor: linked ? 'pointer' : 'default' }}>
{r.m}
{linked ? `${runRow.inv} · ` : ''}{r.v} visits{unpaid ? ` · due` : r.on ? ` · paid ${r.on} via ${RAIL[c.rail].label}` : ''}
{unpaid ? Unpaid : Paid}
); }); })()} {c.balance > 0 && (
{fmtMoney(c.balance)} outstanding across the months above.
)}
{c.svc} {/* pricing & billing schedule — rate, frequency, bill day */} {/* scheduled work & repairs — approved-in-advance loop */} {/* pool water chemistry — history + trust signal */} {c.vert === 'pool' && (
go('o_chem', { id: c.id })} className="card" style={{ marginTop: 10, padding: 13, cursor: 'pointer', borderColor: 'var(--aqua-100)' }}>
Water chemistry
8 visits · CYA trending high
)} {/* equipment on the record */} {c.vert === 'pool' && (
go('o_equipment', { id: c.id })} className="card" style={{ marginTop: 10, padding: 13, display: 'flex', alignItems: 'center', gap: 11, cursor: 'pointer', borderColor: 'var(--aqua-100)' }}>
Equipment
5 parts · pump, filter, heater, salt cell, robot
)}
Pros see "{c.label}" — never the name or price.
{/* assigned worker */}
Assigned to
{w.name}
{w.role}
{/* reassign sheet */} setReassign(false)} title="Reassign to a different Pro">
{c.name.split(' ')[0]}'s {v.name.toLowerCase()} service will move to the new Pro's route. Both Pros are notified and routes rebalance automatically.
{WORKERS.map(nw => { const on = wid === nw.id; return ( ); })}
{/* recent visits */}
Recent verified visits
{visits.map((vi, i) => (
go('h_visit', { id: 'v2' })} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', borderTop: i ? '1px solid var(--ink-100)' : 'none', cursor: 'pointer' }}>
{vi.d}
{vi.tin}–{vi.tout} · {vi.dur} on-site
Verified
))}
); } /* ============================== OWNER · FAB SHEET ==================== */ function OwnerFabSheet({ open, onClose, go, toast }) { const items = [ ['plus', 'green', 'Add customer', () => { go('o_new_customer'); }], ['invoice', 'green', 'New invoice', () => { go('o_new_invoice'); }], ['route', 'blue', 'Dispatch routes', () => { go('o_routes'); }], ['nudge', 'alert', 'Nudge all unpaid', () => { go('o_money'); }], ]; return (
{items.map(([ic, tone, label, fn]) => ( ))}
); } Object.assign(window, { OwnerHome, OwnerMoneyM, OwnerInbox, OwnerThread, OwnerCustomersM, OwnerCustomerM, OwnerFabSheet, MVert });