/* ============================================================================
MOWDEW — Equipment on the record
"Every visit AND every part, on the record." Track the gear at each property
(pump, filter, heater, salt cell, robot) and make it homeowner-visible so age
+ warranty feed the no-surprise-charges repair loop.
• OWNER/PRO OwnerEquipList / EquipDetail — per-property list, add/edit
• HOMEOWNER HomeEquip — calm, proud list + per-item service history
• LOOP equipment → scheduled repair → approved → done → timeline
Reuses the kit (cards, chips, Mono, IconBadge, Sprout/Dewy) + the workitems
flow (WORK_TYPES, WORKITEMS). Loads after mowdew-workitems.jsx.
========================================================================= */
const EQUIP_TYPES = {
pump: { label: 'Pump', life: '8–12 yrs' },
filter: { label: 'Filter', life: '1–2 yrs (cartridge)' },
heater: { label: 'Heater', life: '7–12 yrs' },
salt: { label: 'Salt cell', life: '3–5 yrs' },
robot: { label: 'Robot cleaner', life: '4–6 yrs' },
};
/* Emma's pool gear · 88 Crestline Ave (customer c2) */
const EQUIPMENT = {
c2: [
{ id: 'e1', type: 'pump', make: 'Pentair', model: 'IntelliFlo VSF', installed: 'Mar 2025', age: 'installed 14 months ago',
warranty: { until: 'Mar 2027', covered: true }, scene: 'pool',
history: [{ date: 'Apr 2', label: 'Inspected — running quiet', by: 'Marco' }] },
{ id: 'e2', type: 'filter', make: 'Hayward', model: 'ProGrid · C-7468 cartridge', installed: 'Jan 2024', age: 'installed 2 years ago',
warranty: { until: 'Jan 2025', covered: false }, scene: 'pool', work: 'w1',
history: [{ date: 'Apr 2', label: 'Cartridge rinsed', by: 'Marco' }, { date: 'Jan 12', label: 'Cartridge replaced', by: 'Marco', cents: 18000 }] },
{ id: 'e3', type: 'heater', make: 'Raypak', model: 'Digital 266K BTU', installed: 'May 2019', age: 'installed ~7 years ago',
warranty: { until: 'May 2021', covered: false }, scene: 'pool', nudge: "Your heater's getting on — your pro may suggest a replacement before next winter. No rush.",
history: [{ date: 'Mar 10', label: 'Pilot & vents checked', by: 'Marco' }] },
{ id: 'e4', type: 'salt', make: 'Pentair', model: 'IntelliChlor IC40', installed: 'Apr 2024', age: 'installed 13 months ago',
warranty: { until: 'Apr 2027', covered: true }, scene: 'pool',
history: [{ date: 'Apr 24', label: 'Salt cell cleaned & descaled', by: 'Marco', cents: 9000 }] },
{ id: 'e5', type: 'robot', make: 'Polaris', model: 'VRX iQ+', installed: 'Jun 2025', age: 'installed 11 months ago',
warranty: { until: 'Jun 2027', covered: true }, scene: 'pool', history: [] },
],
};
function equipFor(cid) { return EQUIPMENT[cid] || EQUIPMENT.c2; }
/* ─── Papercraft equipment icons — flat cut-paper, left-lit ─────────── */
function EquipPaper({ type, size = 52 }) {
const s = { width: size, height: size, display: 'block' };
const wrap = (kids) => ;
if (type === 'pump') return wrap(
);
if (type === 'filter') return wrap(
);
if (type === 'heater') return wrap(
);
if (type === 'salt') return wrap(
);
// robot
return wrap(
);
}
/* ─── Warranty chip — green covered / grey expired ──────────────────── */
function WarrantyChip({ warranty, size = 'md' }) {
const big = size === 'lg';
const c = warranty.covered
? ['var(--green-50)', 'var(--green-100)', 'var(--green-700)', 'shield']
: ['var(--ink-50)', 'var(--ink-100)', 'var(--ink-500)', 'clock'];
return (
{warranty.covered ? `WARRANTY TO ${warranty.until.toUpperCase()}` : 'WARRANTY EXPIRED'}
);
}
/* ─── Repair-loop chain — equipment → scheduled → approved → done → timeline ── */
function EquipRepairChain({ stage = 1 }) {
const steps = [['Equipment', 'flask'], ['Scheduled', 'calendar'], ['Approved', 'check'], ['Done', 'shield'], ['Timeline', 'ledger']];
return (
{steps.map(([lb, ic], i) => {
const done = i <= stage;
return (
{i > 0 && }
{lb.toUpperCase()}
);
})}
);
}
/* ─── Equipment row (used by owner/pro list + homeowner list) ───────── */
function EquipRow({ eq, onClick, homeowner }) {
const t = EQUIP_TYPES[eq.type];
return (
{t.label} · {eq.make}
{homeowner ? eq.age : `${eq.model} · ${eq.installed}`}
{onClick &&
}
);
}
/* ============================================================================
OWNER / PRO — Equipment list on the property
========================================================================= */
function OwnerEquipList({ go, params, toast, pro }) {
const cid = (params && params.id) || 'c2';
const c = (window.CUSTOMERS || []).find(x => x.id === cid) || { name: 'Emma Walsh', label: 'Crestline Ave', addr: '88 Crestline Ave', city: 'San Jose' };
const [list, setList] = React.useState(equipFor(cid));
const [add, setAdd] = React.useState(false);
const title = pro ? `${c.label} pool` : c.name;
const sub = pro ? 'Equipment on this property' : `${c.addr} · equipment`;
const back = () => pro ? go('pro_visit', { id: 's2' }) : go('o_customer', { id: cid });
const addItem = (item) => { setList(l => [...l, { ...item, id: 'e' + (l.length + 9), history: [] }]); setAdd(false); toast && toast('Equipment added · on the record', 'green'); };
return (
}>
Private : null} />
{list.length === 0 ? (
No equipment on file yet
Add the pump, filter, heater and salt cell here. Once it's on the record, age & warranty drive the no-surprise repair loop.
) : (
<>
On the record
{list.map((eq, i) => (
go(pro ? 'pro_equip_detail' : 'o_equip_detail', { id: cid, eq: eq.id })} />
))}
{!pro && (
{c.name.split(' ')[0]} sees this list too — what's installed, how old, and what's under warranty. Trust built in.
)}
>
)}
setAdd(false)} onSave={addItem} />
);
}
/* ─── Add / edit equipment form (sheet) ─────────────────────────────── */
function EquipAddSheet({ open, onClose, onSave, initial }) {
const [type, setType] = React.useState(initial ? initial.type : 'pump');
const [make, setMake] = React.useState(initial ? initial.make : '');
const [model, setModel] = React.useState(initial ? initial.model : '');
const [installed, setInstalled] = React.useState(initial ? initial.installed : '');
const [wlen, setWlen] = React.useState(initial ? null : '2 yr');
const [photo, setPhoto] = React.useState(!!initial);
const [notes, setNotes] = React.useState(initial ? (initial.notes || '') : '');
const ready = make.trim() && model.trim() && installed.trim();
const save = () => {
const covered = wlen !== 'None';
onSave({ type, make: make.trim(), model: model.trim(), installed: installed.trim(), notes: notes.trim(), scene: 'pool',
warranty: { until: covered ? '2027' : installed, covered }, age: `installed ${installed}` });
};
return (
Type
{Object.keys(EQUIP_TYPES).map(k => {
const on = type === k;
return (
);
})}
setMake(e.target.value)} style={{ padding: '12px 13px', fontSize: 14 }} />
setModel(e.target.value)} style={{ padding: '12px 13px', fontSize: 14 }} />
setInstalled(e.target.value)} style={{ padding: '12px 13px', fontSize: 14 }} />
Warranty length
{['1 yr', '2 yr', '3 yr', '5 yr', 'None'].map(w => (
))}
);
}
/* ============================================================================
OWNER / PRO — Equipment detail (history + the repair loop)
========================================================================= */
function EquipDetail({ go, params, toast, pro }) {
const cid = (params && params.id) || 'c2';
const eq = equipFor(cid).find(x => x.id === (params && params.eq)) || equipFor(cid)[0];
const t = EQUIP_TYPES[eq.type];
const c = (window.CUSTOMERS || []).find(x => x.id === cid) || { name: 'Emma Walsh', label: 'Crestline Ave' };
const [edit, setEdit] = React.useState(false);
const linkedWork = eq.work ? (window.WORKITEMS || []).find(w => w.id === eq.work) : null;
return (
}>
go(pro ? 'pro_equipment' : 'o_equipment', { id: cid })} />
{/* hero */}
{eq.make} {t.label.toLowerCase()}
{eq.model}
{/* gentle age nudge */}
{eq.nudge && (
)}
{/* the repair loop, made visible */}
{linkedWork && (
Repair on the way
{linkedWork.label} · {fmtMoney(linkedWork.cost)}
{linkedWork.date} · waiting on {c.name.split(' ')[0]}'s OK
SENT
When it's done it appends to this item's history below — equipment, repair, price and proof, all on one record.
)}
{/* service history tied to THIS item */}
Service history
{eq.history.length === 0 ? (
Brand new — nothing to service yet
Every clean, repair or part lands here automatically.
) : (
{eq.history.map((h, i) => (
{h.label}
{h.cents ?
{fmtMoney(h.cents)} : null}
{h.date}{h.by ? ` · ${h.by}` : ''}{h.cents ? ON INVOICE : VERIFIED}
))}
)}
setEdit(false)} onSave={() => { setEdit(false); toast && toast('Equipment updated', 'green'); }} initial={eq} />
);
}
/* ============================================================================
HOMEOWNER — Your equipment (calm, proud, never a hard upsell)
========================================================================= */
function HomeEquip({ go }) {
const list = equipFor('c2');
const covered = list.filter(e => e.warranty.covered).length;
return (
go('h_request')} />}>
88 Crestline Ave
Your equipment
{/* proud summary */}
{list.length} pieces, on the record
{covered} under warranty. Your pro tracks every part's age — so a repair is never a surprise.
{/* the list */}
What's installed
{list.map((eq, i) => (
go('h_equip_detail', { eq: eq.id })} />
{eq.nudge && (
{eq.nudge}
)}
))}
Every part, on the record — added by your pro on a verified visit. Tap any item for its full service history.
);
}
/* HOMEOWNER — per-item detail (history-first, warm) */
function HomeEquipDetail({ go, params }) {
const eq = equipFor('c2').find(x => x.id === (params && params.eq)) || equipFor('c2')[0];
const t = EQUIP_TYPES[eq.type];
return (
go('h_request')} />}>
go('h_equipment')} />
{eq.make} {t.label.toLowerCase()}
{eq.age}
{eq.nudge && (
)}
Service history
{eq.history.length === 0 ? (
Nothing needed yet
It's new — when your pro services it, you'll see it here.
) : (
{eq.history.map((h, i) => (
{h.label}
{h.date}{h.by ? ` · ${h.by}` : ''}
{h.cents ?
{fmtMoney(h.cents)} :
VERIFIED}
))}
)}
);
}
Object.assign(window, {
EQUIP_TYPES, EQUIPMENT, equipFor, EquipPaper, WarrantyChip, EquipRepairChain, EquipRow, EquipAddSheet,
OwnerEquipList, EquipDetail, HomeEquip, HomeEquipDetail,
});