/* ============================================================================
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 */}
May 2026}>Billing · Emma
{inv === 'sent' && (
INV-0007 · Emma Walsh
Emailed with the PDF · 4 verified visits
DUE MAY 26
{world.nudged ? (
Reminder sent — Emma was notified.
) : (
Send respectful nudge
)}
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
Reschedule
toast && toast('Messages live in the full prototype', 'blue')} style={{ flex: 1, minHeight: 42, border: '1px solid var(--green-100)', background: '#fff', color: 'var(--green-800)', borderRadius: 12, fontFamily: 'var(--font-body)', fontWeight: 700, fontSize: 13.5, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, cursor: 'pointer' }}>
Message
{/* 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' && (
act.homeReply(true)} className="btn btn-primary" style={{ flex: 1.3, minHeight: 42 }}> Yes, add it
act.homeReply(false)} className="btn btn-secondary" style={{ flex: 1, minHeight: 42 }}>Not now
)}
{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' && (
Accept {rq.proposedTo}
Keep my original time
)}
{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' &&
nav('h_recap', { id: 'v2' })} className="btn btn-secondary btn-block" style={{ minHeight: 40, marginTop: 11 }}> See the full recap }
{/* 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 }
))}
);
}
/* ───────────────── 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)}% CLEARED BILLED {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 }}>
{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 });