/* ============================================================================ MOWDEW — Billing kit (shared) Recurring billing data + the reusable components behind the billing flows: FreqSelector · BillDayStepper · NextInvoiceNote · TimeToBillCard · DueRow / HeldRow · VisitTag (verified vs logged) · PdfInvoice (US Letter). Used by the owner desktop app, the owner mobile app, the design system page and the standalone PDF / email artifacts. Exposed on window. Load AFTER mowdew-kit.jsx (and mowdew-owner-data.jsx where CUSTOMERS is needed at render time — nothing here touches CUSTOMERS at load time). ========================================================================= */ /* ─── Business profile — what prints on the PDF ─────────────────────── */ const BIZPROFILE = { name: 'Marta Lawn & Pool', owner: 'Diego Marta', addr: '2210 Middlefield Rd', city: 'Palo Alto, CA 94301', phone: '(650) 555-0144', email: 'diego@martalawn.co', zelle: 'diego@martalawn.co', venmo: '@diego-marta', paypal: 'paypal.me/martalawn', }; /* ─── Frequencies ───────────────────────────────────────────────────── */ const FREQS = ['Monthly', 'Every 3 months', 'Every 6 months', 'Yearly', 'Manual']; const FREQ_SHORT = { 'Monthly': 'Monthly', 'Every 3 months': 'Every 3 mo', 'Every 6 months': 'Every 6 mo', 'Yearly': 'Yearly', 'Manual': 'Manual' }; /* Next-due preview for the billing card (prototype world: today = May 12, 2026) */ function nextDueLabel(freq, day) { if (freq === 'Manual') return null; if (freq === 'Monthly') return (day > 12 ? 'May' : 'Jun') + ` ${day}, 2026`; if (freq === 'Every 3 months') return `Aug ${day}, 2026`; if (freq === 'Every 6 months') return `Nov ${day}, 2026`; return `May ${day}, 2027`; } /* ─── The May 12 billing run ──────────────────────────────────────────── Owner-confirmed, no cron: these are the invoices Mowdew has READY, built from recorded visits. Nothing sends until the owner taps the button. */ const BILLRUN = { dateLabel: 'Tuesday, May 12', issued: 'May 12, 2026', dueDate: 'May 26, 2026', rows: [ { cid: 'c2', inv: 'INV-0007', period: 'Apr 12 – May 12, 2026', visits: [ { d: 'Apr 21', verified: true }, { d: 'Apr 28', verified: true }, { d: 'May 5', verified: true }, { d: 'May 12', verified: true }, ] }, { cid: 'c4', inv: 'INV-0008', period: 'Feb 12 – May 12, 2026', visits: [ { d: 'Feb 24', verified: true }, { d: 'Mar 18', verified: false, by: 'Diego' }, { d: 'Apr 12', verified: true }, ] }, { cid: 'c7', inv: 'INV-0009', period: 'Apr 12 – May 12, 2026', visits: [ { d: 'Apr 14', verified: true }, { d: 'Apr 21', verified: true }, { d: 'Apr 28', verified: true }, { d: 'May 5', verified: true }, ] }, ], held: ['c3', 'c8', 'c5', 'c6', 'c1'], }; function runAmount(row, c) { return row.visits.length * c.price; } /* one-shot run state for the prototype: once sent, the action item hides */ function runPending() { return window.__mowdewRunSent ? [] : BILLRUN.rows; } function markRunSent() { window.__mowdewRunSent = true; } function runTotal(customers) { return runPending().reduce((s, r) => { const c = customers.find(x => x.id === r.cid); return s + (c ? runAmount(r, c) : 0); }, 0); } function heldWhy(c) { return c.freq === 'Manual' ? 'Manual — you bill when ready' : `${c.freq} — next due ${c.nextDue}`; } /* ─── FreqSelector — "How often do you bill {Name}?" ───────────────────── compact=false: chip/segment grid (desktop). compact=true: tappable field that expands an inline list (mobile). Aqua = the billing accent. */ function FreqSelector({ value, onChange, compact = false }) { const [open, setOpen] = React.useState(false); if (!compact) { return (
{FREQS.map(f => { const on = value === f; return ( ); })}
); } return (
{open && (
{FREQS.map((f, i) => { const on = value === f; return (
{ onChange(f); setOpen(false); }} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px', borderTop: i ? '1px solid var(--ink-100)' : 'none', cursor: 'pointer', background: on ? 'var(--aqua-50)' : '#fff' }}> {f} {on && }
); })}
)}
); } /* ─── BillDayStepper — "Bill on day N of the month" (1–28) ──────────── */ function BillDayStepper({ day, onChange }) { const Step = ({ dir, label }) => ( ); return (
Bill on day {day} of the month
); } /* ─── NextInvoiceNote — the calm aqua line ──────────────────────────── */ function NextInvoiceNote({ date }) { return (
Next invoice {date}
); } /* ─── SaveButton — primary with a saved ✓ state ─────────────────────── */ function SaveButton({ label, savedLabel = 'Saved', onSave, block = false, style = {} }) { const [state, setState] = React.useState('idle'); // idle | saving | saved const click = () => { if (state !== 'idle') return; setState('saving'); setTimeout(() => { setState('saved'); onSave && onSave(); setTimeout(() => setState('idle'), 2200); }, 550); }; const saved = state === 'saved'; return ( ); } /* ─── TimeToBillCard — the dashboard / money action item ───────────────── Aqua-tinted, calendar glyph. Renders nothing when count is 0 (zero state = hidden). compact = mobile width. */ function TimeToBillCard({ count, total, onOpen, compact = false, style = {} }) { if (!count) return null; return ( ); } /* ─── Checkbox (run rows) ───────────────────────────────────────────── */ function RunCheck({ on }) { return ( {on && } ); } /* ─── DueRow — selectable billing-run row ───────────────────────────── */ function DueRow({ c, row, checked, onToggle, first = false, compact = false }) { const amt = runAmount(row, c); return (
{c.name}
{c.freq} · due May 12
{row.visits.length} visits
); } /* ─── HeldRow — clearly shows WHY someone is skipped ────────────────── */ function HeldRow({ c, first = false, compact = false }) { return (
{c.name}
{heldWhy(c)}
HELD
); } /* ─── VisitTag — the verified / logged wording decision ───────────────── Verified (geofence check-in) -> sky-blue shield "Verified on-site". Manually logged (bad GPS etc) -> neutral ink notes "Logged by {Pro}". Honest both ways: never implies GPS proof it doesn't have, never makes real work look second-class. */ function VisitTag({ verified, by = 'Diego', size = 11.5 }) { if (verified) { return ( Verified on-site ); } return ( Logged by {by} ); } /* ─── Droplet mark — logo-less fallback for the PDF header ──────────── */ function DropletMark({ size = 54 }) { return ( ); } /* ─── PdfInvoice — the printable artifact (US Letter, 816×1056 @96dpi) ─── inv: { inv, period, issued, due, customer:{name,addr,city}, lines:[{d, label, verified, by, cents}], total }. logoless toggles the fallback. */ function PdfInvoice({ inv, biz = BIZPROFILE, logoless = false }) { const mono = { fontFamily: 'var(--font-mono)', fontSize: 9.5, fontWeight: 600, letterSpacing: '.12em', color: 'var(--ink-500)', textTransform: 'uppercase' }; const hasLogged = inv.lines.some(l => !l.verified); return (
{/* header */}
{logoless ? : }
{biz.name}
{biz.addr} · {biz.city}
{biz.phone} · {biz.email}
INVOICE
{inv.inv}
Issued {inv.issued}
Due {inv.due}
{/* bill-to + period */}
Bill to
{inv.customer.name}
{inv.customer.addr} · {inv.customer.city}
Service period
{inv.period}
{inv.lines.length} visits, every one on the record
{/* line items */}
DateServiceRecordAmount
{inv.lines.map((l, i) => (
{l.d} {l.label} {l.verified ? Verified on-site : Logged by {l.by || 'your pro'}} {fmtMoney(l.cents)}
))} {/* total */}
Total due {fmtMoney(inv.total)}
{/* how to pay */}
How to pay
Any of these reaches {biz.owner.split(' ')[0]} directly — please pay by {inv.due}.
{[['zelle', biz.zelle], ['venmo', biz.venmo], ['paypal', biz.paypal]].map(([rail, handle]) => (
{RAIL[rail].label}
{handle}
))}
Payments go straight to {biz.name} — Mowdew never touches the money.
{/* verification footnote */}
Verified on-site — confirmed by an automatic location check-in at your property. {hasLogged && <> · Logged — recorded by your pro when GPS couldn't confirm. Still real work, still on the record.}
{/* footer */}
{logoless ? : } Every visit, on the record. SENT WITH MOWDEW · MOWDEW.COM
); } /* Build a PdfInvoice model from a billing-run row + a CUSTOMERS record */ function invModelFromRun(row, c) { return { inv: row.inv, period: row.period, issued: BILLRUN.issued, due: BILLRUN.dueDate, customer: { name: c.name, addr: c.addr, city: c.city }, lines: row.visits.map(v => ({ d: v.d, label: c.svc, verified: v.verified, by: v.by, cents: c.price })), total: runAmount(row, c), }; } Object.assign(window, { BIZPROFILE, FREQS, FREQ_SHORT, nextDueLabel, BILLRUN, runAmount, heldWhy, runPending, markRunSent, runTotal, FreqSelector, BillDayStepper, NextInvoiceNote, SaveButton, TimeToBillCard, RunCheck, DueRow, HeldRow, VisitTag, DropletMark, PdfInvoice, invModelFromRun, });