/* ============================================================================
MOWDEW — Scheduled work & repairs (trust + transparency)
The "no surprise charges" loop: the OWNER schedules a work item (filter
replacement, repair, custom…) with a date and a cost → the HOMEOWNER gets
it for approval BEFORE anyone shows up → once approved the PRO sees it on
his day and marks it complete → it lands in the homeowner's timeline as a
first-class card, right between regular visits. Heterogeneous timeline:
visits, replacements, repairs — every event on the record.
Loads after mowdew-kit (+ frame/data on mobile). Exposed on window.
========================================================================= */
/* ─── The finite work catalogue (+ custom) ──────────────────────────── */
const WORK_TYPES = [
{ key: 'filter', label: 'Filter replacement', icon: 'filter', tone: 'aqua', cents: 18000, every: 'Every 6 months' },
{ key: 'pump', label: 'Pump repair', icon: 'zap', tone: 'yellow', cents: 24000 },
{ key: 'salt', label: 'Salt cell clean', icon: 'beaker', tone: 'blue', cents: 9000 },
{ key: 'rescue', label: 'Green-to-clean rescue', icon: 'flask', tone: 'green', cents: 30000 },
{ key: 'custom', label: 'Something else', icon: 'notes', tone: 'ink' },
];
const WORK_TONE = {
aqua: { bg: 'var(--aqua-50)', fg: 'var(--aqua-700)', solid: 'var(--aqua-500)' },
yellow:{ bg: 'var(--yellow-50)', fg: 'var(--yellow-700)', solid: 'var(--yellow-500)' },
blue: { bg: 'var(--blue-50)', fg: 'var(--blue-700)', solid: 'var(--blue-500)' },
green: { bg: 'var(--green-50)', fg: 'var(--green-700)', solid: 'var(--green-500)' },
ink: { bg: 'var(--ink-50)', fg: 'var(--ink-700)', solid: 'var(--ink-900)' },
};
/* Emma's work items — one awaiting her OK, one already on the record */
const WORKITEMS = [
{ id: 'w1', type: 'filter', label: 'Filter replacement', date: 'Sat, Jun 14', cost: 18000, state: 'pending',
note: 'Cartridge filters wear out every 6 months — this keeps the water (and the pump) healthy.' },
{ id: 'w2', type: 'salt', label: 'Salt cell clean', date: 'Thu, Apr 24', cost: 9000, state: 'done', by: 'Marco',
note: 'Descaled and inspected — cell is good for another season.' },
];
/* mono record-mark for work states */
function WorkState({ state }) {
const m = {
pending: ['yellow', 'NEEDS YOUR OK'],
sent: ['yellow', 'WAITING ON APPROVAL'],
approved: ['green', 'APPROVED'],
done: ['blue', 'DONE · ON THE RECORD'],
}[state] || ['ink', state.toUpperCase()];
return {m[1]} ;
}
/* ─── HOMEOWNER · upcoming approval card (top of the feed) ──────────── */
function HomeWorkUpcoming({ w = WORKITEMS[0], toast }) {
const [state, setState] = React.useState(w.state); // pending | approved
const t = WORK_TYPES.find(x => x.key === w.type) || WORK_TYPES[0];
const tone = WORK_TONE[t.tone];
return (
{w.label}
{w.date} · {fmtMoney(w.cost)}{t.every ? ` · ${t.every.toLowerCase()}` : ''}
Diego scheduled this — {w.note} Nothing happens (and nothing is billed) until you approve.
{state === 'pending' ? (
{ setState('approved'); toast && toast(`Approved — on the calendar for ${w.date}`, 'green'); }} className="btn btn-primary" style={{ flex: 1.4, minHeight: 44 }}> Approve · {fmtMoney(w.cost)}
toast && toast('Diego will get your question', 'blue')} className="btn btn-secondary" style={{ flex: 1, minHeight: 44 }}> Ask Diego
) : (
Approved — it'll appear in your timeline like any visit. The {fmtMoney(w.cost)} lands on your invoice only after it's done.
)}
);
}
/* ─── HOMEOWNER · completed work card (inside the timeline) ─────────── */
function HomeWorkDone({ w = WORKITEMS[1] }) {
const t = WORK_TYPES.find(x => x.key === w.type) || WORK_TYPES[0];
const tone = WORK_TONE[t.tone];
return (
{w.label}
{w.date} · {w.by} · approved by you
Done
{w.note}
{fmtMoney(w.cost)}
billed on your May invoice
RECORDED
);
}
/* ─── PRO · scheduled work on his day ───────────────────────────────── */
function ProWorkCard({ toast }) {
const [state, setState] = React.useState('approved'); // approved | done
const w = WORKITEMS[0];
const t = WORK_TYPES.find(x => x.key === w.type);
return (
approved by the homeowner}>Scheduled work
Crestline Ave pool · {w.label}
{w.date} · part you need: C-7468 cartridge
{state === 'done' ?
Done :
APPROVED }
{state === 'approved' ? (
<>
Homeowner approved it in advance — do the swap, snap a photo, mark it done.
{ setState('done'); toast && toast('Filter replacement on the record — homeowner can see it', 'green'); }} className="btn btn-dark btn-block" style={{ minHeight: 46, marginTop: 11 }}> Mark complete
>
) : (
On the record — it's already in the homeowner's timeline.
)}
);
}
/* ─── OWNER · "Work & repairs" card on customer detail (mobile) ─────── */
function OwnerWorkCardM({ c, go }) {
const w = WORKITEMS[0];
return (
{w.label} · {fmtMoney(w.cost)}
{w.date} · waiting on {c.name.split(' ')[0]}'s OK
SENT
go('o_schedule_work', { id: c.id })} className="btn btn-secondary btn-block" style={{ minHeight: 42, marginTop: 11 }}> Schedule work or a repair
);
}
/* ─── OWNER · schedule-work screen (mobile) ─────────────────────────── */
function OwnerScheduleWorkM({ go, params, toast }) {
const c = CUSTOMERS.find(x => x.id === (params && params.id)) || CUSTOMERS[1];
const equip = (window.equipFor ? window.equipFor(c.id) : []);
const [eqId, setEqId] = React.useState((params && params.equip) || null);
const [eqFree, setEqFree] = React.useState('');
const eqSel = equip.find(e => e.id === eqId) || null;
const [type, setType] = React.useState('filter');
const [custom, setCustom] = React.useState('');
const [date, setDate] = React.useState('Sat, Jun 14');
const [cost, setCost] = React.useState(18000);
const [sent, setSent] = React.useState(false);
const t = WORK_TYPES.find(x => x.key === type);
const pick = (k) => { setType(k); const wt = WORK_TYPES.find(x => x.key === k); if (wt.cents) setCost(wt.cents); };
const first = c.name.split(' ')[0];
if (sent) {
return (
go('o_customer', { id: c.id })} className="btn btn-primary btn-block">Back to {first} }>
go('o_customer', { id: c.id })} />
Sent for approval
{first} just got it — {type === 'custom' ? (custom || 'Custom work') : t.label}, {date}, {fmtMoney(cost)} . Nothing happens until they approve, and the price they approve is the price they pay. No surprises at the invoice.
{eqSel && (
Linked to {(window.EQUIP_TYPES[eqSel.type] || {}).label} · {eqSel.make} — once it's done, it appends to that item's service history.
)}
);
}
return (
setSent(true)} className="btn btn-primary btn-block" style={{ minHeight: 48 }}> Send to {first} for approval }>
go('o_customer', { id: c.id })} />
{/* customer */}
{c.name}
{c.addr}, {c.city} · {c.svc}
{/* what */}
What's the work?
{WORK_TYPES.map(wt => {
const on = type === wt.key;
const tn = WORK_TONE[wt.tone];
return (
pick(wt.key)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '11px 12px', borderRadius: 13, border: on ? `2px solid ${tn.fg}` : '1.5px solid var(--ink-100)', background: on ? tn.bg : '#fff', cursor: 'pointer', textAlign: 'left', gridColumn: wt.key === 'custom' ? '1 / -1' : 'auto' }}>
{wt.label}
{wt.every && {wt.every} }
);
})}
{type === 'custom' && (
setCustom(e.target.value)} style={{ marginTop: 8, padding: '12px 13px', fontSize: 14 }} />
)}
{/* against which equipment — ties the repair to a tracked part */}
{equip.length > 0 && (
Against which equipment? · optional
{equip.map(e => {
const on = eqId === e.id; const et = window.EQUIP_TYPES[e.type] || {};
return (
{ setEqId(on ? null : e.id); setEqFree(''); }} style={{ flex: '0 0 auto', width: 132, display: 'flex', flexDirection: 'column', gap: 6, padding: '11px 11px', borderRadius: 14, border: on ? '2px solid var(--aqua-500)' : '1.5px solid var(--ink-100)', background: on ? 'var(--aqua-50)' : '#fff', cursor: 'pointer', textAlign: 'left' }}>
{on && }
{et.label} · {e.make}
{e.model}
);
})}
{/* freeform — something not on the record yet */}
{ setEqId('free'); }} style={{ flex: '0 0 auto', width: 132, display: 'flex', flexDirection: 'column', gap: 6, padding: '11px 11px', borderRadius: 14, border: eqId === 'free' ? '2px solid var(--aqua-500)' : '1.5px dashed var(--ink-200)', background: eqId === 'free' ? 'var(--aqua-50)' : '#fff', cursor: 'pointer', textAlign: 'left' }}>
Something else
Not on the record
{eqId === 'free' && (
setEqFree(e.target.value)} placeholder="Name the part — e.g. waterline tile, deck drain" style={{ padding: '12px 13px', fontSize: 14 }} />
)}
{eqSel &&
When done, this appends to {eqSel.make} {(window.EQUIP_TYPES[eqSel.type] || {}).label.toLowerCase()}'s service history.
}
{eqId === 'free' && eqFree.trim() &&
Logged against "{eqFree.trim()}" — you can add it to the equipment record later.
}
)}
{/* when */}
When?
{['Sat, Jun 14', 'Tue, Jun 17', 'Sat, Jun 21'].map(dd => (
setDate(dd)} style={{ padding: '9px 13px', borderRadius: 11, border: date === dd ? '2px solid var(--green-500)' : '1.5px solid var(--ink-200)', background: date === dd ? 'var(--green-50)' : '#fff', color: date === dd ? 'var(--green-800)' : 'var(--ink-700)', fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 700, cursor: 'pointer' }}>{dd}
))}
{/* cost */}
Cost — {first} approves exactly this
$
setCost(Math.round((+e.target.value.replace(/[^0-9]/g, '') || 0) * 100))} inputMode="numeric" style={{ flex: 1, border: 'none', outline: 'none', padding: '13px 0', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 20, background: 'transparent', width: '100%' }} />
parts & labor
{first} approves before anyone shows up; it bills only after the pro marks it done. That's the whole point — no end-of-year surprises.
);
}
/* ─── OWNER · desktop “Work & repairs” card (inline schedule form) ──── */
function OwnerWorkCardDesk({ c, toast }) {
const [open, setOpen] = React.useState(false);
const [type, setType] = React.useState('filter');
const [date, setDate] = React.useState('Sat, Jun 14');
const [cost, setCost] = React.useState(18000);
const [items, setItems] = React.useState([{ ...WORKITEMS[0] }]);
const first = c.name.split(' ')[0];
const send = () => {
const t = WORK_TYPES.find(x => x.key === type);
setItems(it => [{ id: 'w' + (it.length + 9), type, label: t.label, date, cost, state: 'sent' }, ...it]);
setOpen(false);
toast && toast(`Sent to ${first} for approval — ${t.label}, ${fmtMoney(cost)}`, 'aqua');
};
return (
Work & repairs
{items.map(w => (
x.key === w.type) || WORK_TYPES[0]).icon} tone="aqua" size={34} />
{w.label} · {fmtMoney(w.cost)}
{w.date}
WAITING ON {first.toUpperCase()}
))}
{!open ? (
setOpen(true)} className="btn btn-secondary" style={{ marginTop: 12, minHeight: 42 }}> Schedule work or a repair
) : (
{WORK_TYPES.map(wt => {
const on = type === wt.key; const tn = WORK_TONE[wt.tone];
return { setType(wt.key); if (wt.cents) setCost(wt.cents); }} style={{ display: 'inline-flex', alignItems: 'center', gap: 7, padding: '8px 12px', borderRadius: 11, border: on ? `2px solid ${tn.fg}` : '1.5px solid var(--ink-200)', background: on ? tn.bg : '#fff', color: on ? tn.fg : 'var(--ink-700)', fontSize: 12.5, fontWeight: 700, cursor: 'pointer' }}> {wt.label} ;
})}
{['Sat, Jun 14', 'Tue, Jun 17'].map(dd => (
setDate(dd)} style={{ padding: '8px 11px', borderRadius: 10, border: date === dd ? '2px solid var(--green-500)' : '1.5px solid var(--ink-200)', background: date === dd ? 'var(--green-50)' : '#fff', fontFamily: 'var(--font-mono)', fontSize: 11.5, fontWeight: 700, color: date === dd ? 'var(--green-800)' : 'var(--ink-700)', cursor: 'pointer' }}>{dd}
))}
$
setCost(Math.round((+e.target.value.replace(/[^0-9]/g, '') || 0) * 100))} style={{ width: 56, border: 'none', outline: 'none', padding: '8px 0', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 15, background: 'transparent' }} />
Send for approval
Bills only after it's done — the price {first} approves is the price {first} pays.
)}
);
}
Object.assign(window, { WORK_TYPES, WORK_TONE, WORKITEMS, WorkState, HomeWorkUpcoming, HomeWorkDone, ProWorkCard, OwnerWorkCardM, OwnerScheduleWorkM, OwnerWorkCardDesk });