/* ============================================================================ MOWDEW LIVE DEMO — per-tab screens, cloned from the App.html prototypes Each is the REAL app screen's layout (same headers, cards, components), re-wired to the shared demo `world` where the sync storyline needs it. Static tabs (Pro Done/Notes/Settings, Owner Customers) render the actual app components directly from the phone shells. ========================================================================= */ /* ───────────────────────── OWNER · MONEY ───────────────────────────── */ function OwnerMoneyTab({ world, act, nav }) { const inv = world.invoice; // null | 'sent' | 'paid' const ready = world.visit === 'done'; const railLbl = (RAIL[world.paidVia] || RAIL.zelle).label; return ( {}} />}> {/* summary — paid vs owed */}
Collected · May
Outstanding
May 2026}>Billing · Emma {inv === 'sent' && (
INV-0007 · Emma Walsh
Emailed with the PDF · 4 verified visits
DUE MAY 26
Amount due
{world.nudged ? (
Reminder sent — Emma was notified.
) : ( )}
Your Zelle, Venmo & PayPal print on the PDF — Mowdew never touches the money.
)} {inv === 'paid' && (
INV-0007 · Emma Walsh
Paid the same day it was sent
{`PAID · ${railLbl.toUpperCase()}`}
All squared up — $260.00 went straight to your {railLbl}. Mowdew never touched it.
)}
May so far · Emma
{world.visit === 'done' ? '4 verified visits' : '3 verified visits'}{inv === 'paid' ? ' · collected' : ''}
Marco never sees this tab — Pros are money-blind by design.
); } /* ───────────────────────── HOME · VISIT FEED (app HomeFeed clone) ──── */ function HomeFeedTab({ world, act, nav, openRequest, openReq, toast }) { const v = world.visit; const rq = world.resched; return ( }> {/* header — same as the app's feed */}
88 Crestline Ave
Your visits
{/* provider strip — with the request actions, same as the app */}
Diego Marta
Marta Lawn & Pool · Weekly pool
4.9
{/* approve-before-work — request lifecycle */} {/* reviews — fires once the month is paid (peak trust) */} {/* respectful nudge — lands as a banner, not just a toast */} {world.nudged && world.invoice === 'sent' && (
nav('h_inbox')} style={{ display: 'flex', alignItems: 'center', gap: 11, background: 'var(--yellow-50)', border: '1px solid var(--yellow-100)', borderRadius: 'var(--radius-lg)', padding: '12px 14px', cursor: 'pointer', boxShadow: 'var(--shadow-sm)' }}>
A friendly reminder from Diego
INV-0007 · $260.00 · due May 26 — pay in one tap
)} {/* message from Diego — the field-note follow-up */} {(world.note === 'asked' || world.note === 'replied' || world.note === 'closed') && (
Marta Lawn & Pool
{world.note === 'asked' && NEW MESSAGE} {world.note === 'replied' && SENT} {world.note === 'closed' && DONE}
Marco says you'd like a monthly filter deep-clean — I can add it for $20/visit. Want it?
{(world.note === 'replied' || world.note === 'closed') && (
{world.noteYes ? 'Yes please — add it.' : 'Not right now, thanks.'}
)}
{world.note === 'asked' && (
)} {world.note === 'closed' && world.noteYes && (
Added to your weekly service — starts next visit.
)}
)} {/* live negotiation — appears once a request exists */} {rq && (
Marta Lawn & Pool
{(() => { const [t, l, p] = rqMark(rq.state, 'home'); return {l}; })()}
{rq.state === 'pending' && } {rq.state === 'proposed' && (
)} {rq.state === 'approved' && } {rq.state === 'declined' && }
)} {/* tagline + timeline — same as the app */}
Every visit, on the record
GPS check-in proves when Marco arrived and left — no honor system.
{/* today's visit — live, world-wired */}
Pool service
Today · May 12
{v === 'scheduled' && TODAY · 10 AM} {v === 'driving' && ON THE WAY} {v === 'onsite' && ON-SITE} {v === 'done' && DONE}
{(v === 'onsite' || v === 'done') && (
{v === 'done' ? : In progress}
)}
{v === 'scheduled' && You'll see Marco the moment he's en route} {v === 'driving' && Marco is about 6 min out} {(v === 'onsite' || v === 'done') && <> {world.checkInAt}{world.checkOutAt ? `–${world.checkOutAt}` : ''} {dmClock(world.sec)} } {(v === 'onsite' || v === 'done') && AUTO CHECK-IN}
{v === 'done' && }
{/* past visits — the full 4-week record behind INV-0007 */} {FEED.slice(1).map((fv, i) => nav('h_recap', { id: fv.id })} alt={i % 2 === 0} />)}
); } /* ───────────────────────── HOME · POOL (app HomeChemistry clone) ───── */ function HomePoolTab({ world, nav, openRequest }) { const live = world.chem && world.chem !== 'scanning' ? world.chem : null; const log = live ? [{ id: 'k0', date: 'Today · May 12', ph: live.ph, cl: live.cl, alk: live.alk, live: true }, ...CHEM_LOG.slice(1)] : CHEM_LOG.slice(1); const latest = log[0]; const phSeries = log.map(r => r.ph).reverse(); return ( }>
88 Crestline Ave
Pool chemistry
{/* latest reading hero — same as the app */}
Latest reading {latest.live ? CAPTURED TODAY : {latest.date.toUpperCase()}}
pH BALANCE
{latest.ph.toFixed(1)}
Perfectly balanced
{latest.live ? 'Read from the test strip Marco photographed — moments ago.' : 'Captured from a photo of your test strip on each verified visit.'}
{/* equipment — the REAL app screen, reachable (auto-synced with the app) */}
nav('h_equipment')} className="card" style={{ padding: 14, display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
Your equipment
Pump, filter, heater & more — age, warranty & history
{/* pH trend — same as the app */}
6 weeks in range}>pH over time
{/* reading history — same as the app */}
Every reading, on the record
{log.map((r, i) => (
{r.date}
CL {r.cl.toFixed(1)} · ALK {r.alk}
{r.live && NEW}
{r.ph.toFixed(1)}
pH
))}
); } /* ───────────────── HOME · INVOICES (app HomeInbox clone, world-wired) ─ */ function HomeInvoicesTab({ world, act, nav, openRequest }) { const state = world.invoice; // null | 'sent' | 'paid' const inv = INVOICES[0]; const [rail, setRail] = React.useState(null); const [sheet, setSheet] = React.useState(false); const paid = state === 'paid'; const paidRail = world.paidVia || 'zelle'; const confirmPay = () => { setSheet(false); act.payInvoice(rail || 'zelle'); }; return ( <> }> {!state && (
No open invoices.
The moment Diego sends one, it lands here — same numbers as the PDF in your email.
)} {state && (
{world.nudged && !paid && (
Diego sent a friendly reminder — this one's due May 26.
)}
{inv.biz}
{inv.id} · DUE {inv.due.toUpperCase()}
{paid ? PAID : {`DUE ${inv.due.toUpperCase()}`}}
{paid ? 'Paid in full' : 'Amount due'}
{/* lines — the same verified visits */}
The same 4 verified visits from your feed
{inv.lines.map((l, i) => (
{l.label}
{l.date} · Verified
))}
{/* pay module — same three rails as the app */} {!paid ? (
Pay Diego in one tap
{PROVIDER.rails.map(r => { setRail(r); setSheet(true); }} />)}
Mowdew never touches your money.
) : (
Paid via {RAIL[paidRail].label}
Marked in your ledger — you won't pay this twice.
)}
{!paid && (
Pay once here and it's marked in your ledger — you'll never pay twice.
)}
)}
{/* slim confirm sheet — same as the app */} setSheet(false)} title={`Pay with ${rail ? RAIL[rail].label : ''}`}>
Mowdew opens {rail ? RAIL[rail].label : 'the app'} with the amount and Diego prefilled — confirm there to finish.
{rail ? RAIL[rail].label : ''}
{rail === 'venmo' ? PROVIDER.venmo : rail === 'paypal' ? 'pay.mowdew.co/diego' : PROVIDER.zelle}
Mowdew never touches your money.
); } /* ──────────── HOME · LEDGER (app HomeLedger statement clone) ────────── */ function HomeLedgerTab({ world, nav, openRequest }) { const mayPaid = world.invoice === 'paid'; const rows = LEDGER.map(r => r.id === 'l1' ? (mayPaid ? { ...r, status: 'paid', rail: world.paidVia || 'zelle', paidOn: 'May 12' } : r) : r); const paidYTD = rows.filter(r => r.status === 'paid').reduce((s, r) => s + r.cents, 0); const due = rows.filter(r => r.status === 'unpaid').reduce((s, r) => s + r.cents, 0); const billed = paidYTD + due; const paidCount = rows.filter(r => r.status === 'paid').length; return ( }> {/* statement header — same as the app */}
PAYMENT STATEMENT
Your ledger
2026
{/* statement summary band — same dark artifact */}
CLEARED · 2026
{fmtMoney(paidYTD)}
{paidCount} months cleared
due && nav('h_inbox')} style={{ flex: 1, paddingLeft: 16, borderLeft: '1px solid rgba(255,255,255,.16)', cursor: due ? 'pointer' : 'default' }}>
STILL DUE
{fmtMoney(due)}
{rows.length - paidCount} invoice{rows.length - paidCount === 1 ? '' : 's'} open
{Math.round((paidYTD / billed) * 100)}% CLEAREDBILLED {fmtMoney(billed)}
{/* ledger entries — same row anatomy as the app */}
{rows.map(r => { const unpaid = r.status === 'unpaid'; const [mo, yr] = r.month.split(' '); return (
unpaid && nav('h_inbox')} className="card" style={{ padding: '12px 13px', cursor: unpaid ? 'pointer' : 'default', display: 'flex', alignItems: 'center', gap: 12 }}>
{mo.slice(0, 3)}
{yr}
{r.visits} visits {unpaid ? DUE : CLEARED}
{unpaid ? {r.invoice} : <>Paid {r.paidOn} · {RAIL[r.rail].label}}
); })} {world.invoice === null && (
May's invoice arrives here when Diego sends it — built from the same verified visits.
)}
); } Object.assign(window, { OwnerMoneyTab, HomeFeedTab, HomePoolTab, HomeInvoicesTab, HomeLedgerTab });