/* ============================================================================ MOWDEW — Water chemistry across all three personas The pool's chemistry doubles as a HOMEOWNER TRUST signal: competitors keep it pro-side; Mowdew shows the homeowner a plain, reassuring verdict. • PRO ProLogChem — fast numeric entry inside a visit (mobile) • HOME ChemVerdictHero — "Water: Balanced ✓" star card (used by h_chem) • OWNER OwnerChemHistory — per-property reading timeline + chronic flag • OWNER OwnerConfigReadings — toggle tracked readings / units / ideal ranges Extends the chemistry primitives in mowdew-kit.jsx (READINGS, computeLSI…). ========================================================================= */ /* 8 verified visits for 88 Crestline Ave (Emma) — newest first. Two early blips the pro already handled; CYA quietly creeping high (chronic, owner-side). */ const POOL_CHEM_LOG = [ { id: 'k1', date: 'Today · May 12', short: 'May 12', visit: 'v1', live: true, ph: 7.4, cl: 1.8, alk: 100, cya: 58, salt: 3200, ch: 300, temp: 80 }, { id: 'k2', date: 'Mon · May 5', short: 'May 5', visit: 'v2', ph: 7.5, cl: 2.1, alk: 110, cya: 55, salt: 3150, ch: 290, temp: 79 }, { id: 'k3', date: 'Mon · Apr 28', short: 'Apr 28', visit: 'v3', handled: 'cl', ph: 7.2, cl: 0.8, alk: 95, cya: 53, salt: 3100, ch: 295, temp: 78 }, { id: 'k4', date: 'Mon · Apr 21', short: 'Apr 21', visit: 'v4', handled: 'ph', ph: 7.8, cl: 2.6, alk: 120, cya: 50, salt: 3250, ch: 310, temp: 77 }, { id: 'k5', date: 'Mon · Apr 14', short: 'Apr 14', visit: 'v4', ph: 7.6, cl: 1.9, alk: 105, cya: 47, salt: 3200, ch: 300, temp: 76 }, { id: 'k6', date: 'Mon · Apr 7', short: 'Apr 7', visit: 'v4', ph: 7.3, cl: 1.2, alk: 90, cya: 45, salt: 3050, ch: 285, temp: 75 }, { id: 'k7', date: 'Mon · Mar 31', short: 'Mar 31', visit: 'v4', ph: 7.5, cl: 2.0, alk: 100, cya: 46, salt: 3180, ch: 305, temp: 74 }, { id: 'k8', date: 'Mon · Mar 24', short: 'Mar 24', visit: 'v4', ph: 7.4, cl: 1.6, alk: 108, cya: 44, salt: 3120, ch: 300, temp: 73 }, ]; /* default tracked set + which readings the homeowner verdict leans on */ const CHEM_TRACKED = ['ph', 'cl', 'alk', 'cya', 'salt', 'ch', 'temp']; const CHEM_SANITIZER = ['ph', 'cl', 'alk']; // the readings that drive the homeowner verdict function chemTone(key, val) { const st = chemState(key, val); return st === 'ideal' ? 'ideal' : st === 'low' ? 'low' : st === 'high' ? 'high' : 'empty'; } const CHEM_STATE_C = { ideal: { bg: 'var(--green-50)', bd: 'var(--green-100)', fg: 'var(--green-700)' }, low: { bg: 'var(--yellow-50)', bd: 'var(--yellow-100)', fg: 'var(--yellow-700)' }, high: { bg: '#FBEAE3', bd: '#F6D6C8', fg: 'var(--coral-700,#C2410C)' }, empty: { bg: 'var(--ink-50)', bd: 'var(--ink-100)', fg: 'var(--ink-500)' }, }; /* ─── LSI chip — balance verdict, labelled an estimate ──────────────── */ function LsiChip({ lsi, size = 'md' }) { const v = lsiVerdict(lsi); const c = v.key === 'balanced' ? ['var(--green-50)', 'var(--green-100)', 'var(--green-700)'] : v.key === 'corrosive' ? ['var(--yellow-50)', 'var(--yellow-100)', 'var(--yellow-700)'] : v.key === 'scaling' ? ['var(--chem-50)', '#DDD7FA', '#5847d6'] : ['var(--ink-50)', 'var(--ink-100)', 'var(--ink-500)']; const big = size === 'lg'; return ( {v.label} {lsi != null && LSI {lsi >= 0 ? '+' : ''}{lsi.toFixed(2)}} EST ); } /* ─── HOMEOWNER verdict hero — the star. Plain, reassuring, never scary. ─ */ function ChemVerdictHero({ reading, size = 'lg' }) { const lsi = computeLSI(reading); const v = lsiVerdict(lsi); // any sanitizer reading out of range? const outs = CHEM_SANITIZER.filter(k => reading[k] != null && chemState(k, reading[k]) !== 'ideal'); const handledKey = reading.handled; const allGood = outs.length === 0 && v.key === 'balanced'; const headline = allGood ? 'Water: Balanced' : v.key === 'balanced' ? 'Water: Looked after' : `Water: ${v.label}`; let line; if (allGood) line = "Everything's in range — your pool's healthy."; else if (handledKey) { const r = READINGS[handledKey]; const word = handledKey === 'cl' ? 'Chlorine was a touch low today' : handledKey === 'ph' ? 'pH ran a little high today' : `${r.label} was off today`; line = `${word} — Diego ${handledKey === 'cl' ? 'added tablets' : 'balanced it'} on the visit. All set now.`; } else if (outs.length) { line = "A couple of levels are settling in — your pro's on it. Nothing for you to do."; } else line = "Your pool's in good shape."; return (
{headline} {allGood && }
{(reading.date || 'TODAY').toUpperCase()} · VERIFIED ON-SITE

{line}

Water-balance estimate · your pro confirms it
); } /* ─── Reading display row — used by homeowner detail + owner timeline ── */ function ChemRow({ rkey, value, note, showBand = true }) { const r = READINGS[rkey]; const st = chemState(rkey, value); const c = CHEM_STATE_C[st]; const txt = st === 'ideal' ? 'In range' : st === 'low' ? 'Low' : st === 'high' ? 'High' : '—'; return (
{r.label}
{showBand ? `IDEAL ${r.band}` : ''}{note ? (showBand ? ' · ' : '') + note : ''}
{value == null ? '—' : value.toFixed(r.dp)}{r.unit && value != null ? {r.unit} : null}
{txt.toUpperCase()}
); } /* ============================================================================ PRO — Log water chemistry (mobile, inside a visit) ========================================================================= */ function ProLogChem({ go, params, toast }) { const stop = (window.PRO_STOPS || []).find(s => s.id === params.id) || { label: 'Crestline Ave', vert: 'pool', in: '10:12 AM' }; const tracked = CHEM_TRACKED; const [vals, setVals] = React.useState(() => Object.fromEntries(tracked.map(k => [k, '']))); const [photo, setPhoto] = React.useState(false); const [added, setAdded] = React.useState({}); // dosing keys marked done const set = (k, v) => setVals(s => ({ ...s, [k]: v })); const num = (k) => { const n = parseFloat(vals[k]); return isNaN(n) ? null : n; }; const reading = Object.fromEntries(tracked.map(k => [k, num(k)])); const entered = tracked.filter(k => num(k) != null); const filled = entered.length; const lsi = reading.ph != null ? computeLSI(reading) : null; const outKeys = tracked.filter(k => num(k) != null && chemState(k, num(k)) !== 'ideal'); const dosing = dosingFor(reading).filter(d => !added[d.key]); const allIn = filled === tracked.length && outKeys.length === 0; const scan = () => { // "snap test kit" autofill — one reading comes in a touch low to show the calm out-of-range + dosing flow setVals({ ph: '7.4', cl: '0.8', alk: '100', cya: '45', salt: '3200', ch: '300', temp: '80' }); setPhoto(true); toast('Test kit read · check the values', 'blue'); }; const markAdded = (d) => { setAdded(a => ({ ...a, [d.key]: true })); // optimistic: bring that reading into range so the celebratory state can show if (d.key === 'cl') set('cl', '2.0'); if (d.key === 'ph') set('ph', '7.4'); if (d.key === 'alk') set('alk', '100'); toast('Marked as added · re-test after circulation', 'green'); }; const save = () => { toast('Readings saved · shared with the homeowner', 'green'); go('pro_visit', { id: params.id }); }; return ( }> go('pro_visit', { id: params.id })} right={Private} /> {/* scan / photo */}
{photo ? 'Test kit photo attached' : 'Snap the test kit'}
{photo ? 'Values pre-filled — confirm below' : 'Auto-fills the readings · or enter by hand'}
{photo ? : }
{/* numeric entry list */}
Readings
{tracked.map((k, i) => set(k, v)} top={i > 0} />)}
Saved readings are shared with the homeowner as a plain verdict.
{/* state banner */} {filled === 0 ? (
Enter readings as you test — or snap the kit to fill them in. The balance verdict appears here.
) : ( <> {/* LSI balance verdict */}
Water balance
LANGELIER INDEX · ESTIMATE
{/* all-in-range celebration OR calm out-of-range */} {allIn ? (
All readings in range
Nice — this pool's dialed in. Save and the homeowner sees a clean bill.
) : outKeys.length > 0 && (
{outKeys.length} TO ADJUST · NOT URGENT
{dosing.length > 0 ? (
{dosing.map(d => (
{d.text}
{d.why} · EST for ~15k gal
))}
) : (
Treatments marked added. Re-test after the water circulates.
)}
)} )}
); } /* fast numeric entry row — label + big value + unit + ideal band + live tint */ function ChemEntryRow({ rkey, value, onChange, top }) { const r = READINGS[rkey]; const n = value === '' || value == null ? null : parseFloat(value); const st = n == null || isNaN(n) ? 'empty' : chemState(rkey, n); const c = CHEM_STATE_C[st]; return (
{r.label}
{st === 'empty' ? `IDEAL ${r.band}` : st === 'ideal' ? 'IN RANGE' : st === 'low' ? `LOW · IDEAL ${r.band}` : `HIGH · IDEAL ${r.band}`}
onChange(e.target.value.replace(/[^0-9.]/g, ''))} placeholder="—" aria-label={r.label} style={{ width: 74, textAlign: 'right', border: `1.5px solid ${st === 'empty' ? 'var(--ink-200)' : c.bd}`, borderRadius: 11, padding: '9px 10px', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 20, letterSpacing: '-0.02em', color: st === 'empty' ? 'var(--ink-900)' : c.fg, background: '#fff', outline: 'none', fontVariantNumeric: 'tabular-nums' }} /> {r.unit ? {r.unit} : }
); } /* ============================================================================ OWNER — Chemistry history per property (web + mobile) ========================================================================= */ function chronicFlags(log) { const recent = log.slice(0, 6); const flags = {}; CHEM_TRACKED.forEach(k => { if (READINGS[k].soft) return; const out = recent.filter(r => r[k] != null && chemState(k, r[k]) !== 'ideal').length; if (out >= 3) flags[k] = out; }); return flags; } function OwnerChemHistory({ go, params, toast }) { const log = POOL_CHEM_LOG; const cols = ['ph', 'cl', 'alk', 'cya']; // compact grid columns; full set in expand const [openId, setOpenId] = React.useState(null); const flags = chronicFlags(log); const flagKeys = Object.keys(flags); const latest = log[0]; return ( }> go('o_customer', { id: params.id || 'c2' })} /> {/* latest snapshot */}
Latest · {latest.date.replace('Today · ', '')}
Verified on-site
{cols.map(k => { const st = chemState(k, latest[k]); const c = CHEM_STATE_C[st]; return (
{READINGS[k].abbr}
{latest[k].toFixed(READINGS[k].dp)}
); })}
{/* chronic warning */} {flagKeys.length > 0 && (
{flagKeys.map(k => READINGS[k].label).join(' & ')} chronically out of range
{flagKeys.includes('cya') ? 'CYA has crept above 50 ppm for three visits running — stabilizer builds up over time. Plan a partial drain & refill to reset it.' : 'Out of range on 3+ recent visits — worth a closer look on the next service.'}
)} {/* per-visit timeline */}
Every visit, on the record
{log.map((r, i) => { const open = openId === r.id; const outs = cols.filter(k => chemState(k, r[k]) !== 'ideal'); return (
setOpenId(open ? null : r.id)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '11px 14px', cursor: 'pointer', background: open ? 'var(--ink-50)' : '#fff' }}>
{r.short}
{r.live &&
TODAY
}
{/* compact cells */}
{cols.map(k => { const st = chemState(k, r[k]); const c = CHEM_STATE_C[st]; return (
{READINGS[k].abbr}
{r[k].toFixed(READINGS[k].dp)}
); })}
{open && (
{CHEM_TRACKED.map((k, j) => )}
{r.handled && (
Handled on-site — {READINGS[r.handled].label} brought back in range that visit.
)}
)}
); })}
); } /* ============================================================================ OWNER — Configure readings (setting): toggle tracked, units, ideal ranges ========================================================================= */ function OwnerConfigReadings({ go, toast }) { const POOL_DEFAULT = ['ph', 'cl', 'alk', 'cya', 'salt', 'ch', 'temp']; const [on, setOn] = React.useState(() => Object.fromEntries(CHEM_TRACKED.map(k => [k, true]))); const toggle = (k) => setOn(s => ({ ...s, [k]: !s[k] })); const count = Object.values(on).filter(Boolean).length; return ( }> go('o_settings')} />
{count} readings tracked. Sensible pool defaults are on. Toggle what your crew measures, with ideal ranges.
Tracked readings
{CHEM_TRACKED.map((k, i) => { const r = READINGS[k]; const active = on[k]; return (
{r.abbr}
{r.label}
{r.band}{r.soft ? ' · reference only' : ''}
toggle(k)} tone="green" />
); })}
Ranges follow industry pool norms. The homeowner only ever sees a plain verdict — not the raw set.
); } Object.assign(window, { POOL_CHEM_LOG, CHEM_TRACKED, CHEM_SANITIZER, chemTone, CHEM_STATE_C, LsiChip, ChemVerdictHero, ChemRow, ChemEntryRow, ProLogChem, OwnerChemHistory, OwnerConfigReadings, chronicFlags, });