/* ============================================================================
MOWDEW — The Visit Recap
Mowdew's answer to the beloved photo+chemistry service email — but in-app,
two-way, and SHAREABLE. It's the canonical object an invoice line points to.
A keepsake of "look what got done", not a receipt.
• HOMEOWNER VisitRecap (primary) — share → branded, EXIF-stripped image
• OWNER/PRO the SAME recap surface (viewer flag; name-hidden for pro)
Reuses the kit + ChemVerdictHero (chemistry feature). Loads after the
homeowner screens; exposed on window.
========================================================================= */
/* per-visit recap content. Pool visits carry chemistry; garden visits don't. */
const RECAPS = {
v2: { svc: 'pool', date: 'Mon, May 5', in: '10:04a', out: '10:41a', dur: '37 min', pro: 'Diego', label: 'Crestline Ave pool',
photos: true, before: 'pool-before', after: 'pool-clean',
tasks: ['Skimmed surface & emptied baskets', 'Brushed walls & steps', 'Vacuumed floor', 'Backwashed filter', 'Tested & balanced water'],
chem: { ph: 7.4, cl: 1.8, alk: 100, cya: 45, ch: 300, temp: 80, date: 'Mon, May 5' },
note: 'Filter looking good. Saw the gate latch is a little loose — flagged it for the office.' },
v3: { svc: 'pool', date: 'Mon, Apr 28', in: '9:58a', out: '10:25a', dur: '27 min', pro: 'Diego', label: 'Crestline Ave pool',
photos: true, before: 'pool-before', after: 'pool-clean',
tasks: ['Skimmed surface', 'Brushed walls', 'Emptied baskets', 'Tested water — corrected low chlorine'],
chem: { ph: 7.2, cl: 0.8, alk: 95, cya: 53, ch: 295, temp: 78, date: 'Mon, Apr 28', handled: 'cl' },
note: 'Chlorine was a touch low — added tablets, all set now.' },
v4: { svc: 'pool', date: 'Mon, Apr 21', in: '10:20a', out: '10:42a', dur: '22 min', pro: 'Diego', label: 'Crestline Ave pool',
photos: false, before: 'pool-before', after: 'pool-clean',
tasks: ['Skimmed surface', 'Brushed walls', 'Emptied baskets', 'Tested & balanced water'],
chem: { ph: 7.5, cl: 2.0, alk: 105, cya: 50, ch: 305, temp: 77, date: 'Mon, Apr 21' },
note: '' },
g1: { svc: 'mow', date: 'Mon, May 12', in: '9:06a', out: '9:41a', dur: '35 min', pro: 'Diego', label: 'Oak Ridge Ct garden',
photos: true, before: 'lawn-before', after: 'lawn-after',
tasks: ['Mowed front & back', 'Edged all borders', 'Blew down paths & patio', 'Bagged clippings'],
chem: null,
note: 'Lawn\u2019s filling in nicely with the warmer weather. Bay-friendly mow height for summer.' },
};
function getRecap(id) { return RECAPS[id] || RECAPS.v2; }
/* ─── Before/After compare — draggable divider (keepsake, not a receipt) ─ */
function BeforeAfter({ before, after, height = 230 }) {
const [pct, setPct] = React.useState(52);
return (
{/* after = base layer (full) */}
{/* before = clipped from the left to pct */}
{/* labels */}
BEFORE
AFTER
{/* divider */}
setPct(+e.target.value)} aria-label="Drag to compare before and after"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0, cursor: 'ew-resize', margin: 0 }} />
);
}
/* ─── The recap surface ─────────────────────────────────────────────── */
function VisitRecap({ go, params, toast, viewer = 'homeowner' }) {
const id = (params && params.id) || 'v2';
const r = getRecap(id);
const isPool = r.svc === 'pool';
const nameHidden = viewer === 'pro';
const [share, setShare] = React.useState(false);
const [shared, setShared] = React.useState(false);
const back = viewer === 'owner' ? () => go('o_customer', { id: 'c2' }) : viewer === 'pro' ? () => go('pro_today') : () => go('h_feed');
const accent = isPool ? 'aqua' : 'green';
return (
Done
setShare(true)} className="btn btn-primary" style={{ flex: 1.5 }}> {shared ? 'Share again' : 'Share this recap'}
}>
Shared : null} />
{/* header — date, time on-site, verified, pro */}
{nameHidden
?
:
}
{r.date}
{nameHidden ? r.label : `${r.pro} · ${isPool ? 'pool service' : 'garden service'}`}
Verified
{r.in}–{r.out}
{r.dur} on-site
40 FT
{/* before / after */}
Before & after
{r.photos ? (
) : (
No photos this visit
{r.pro} didn't snap photos this time — the verified check-in & tasks below are still on the record.
)}
{/* tasks completed */}
What got done
{r.tasks.map((t, i) => (
{t}
))}
{/* chemistry verdict — pool only */}
{isPool && r.chem && (
Water
)}
{/* pro note */}
{r.note && (
Note from {nameHidden ? 'the pro' : r.pro}
{nameHidden ?
:
}
{r.note}
)}
{/* keepsake footer */}
{viewer === 'homeowner' ? 'Proud of the pool? Share this recap with family — or post it.' : 'This is exactly what the homeowner sees — every visit, on the record.'}
{viewer === 'homeowner' &&
setShare(true)} className="btn btn-primary" style={{ minHeight: 40, padding: '0 15px', flexShrink: 0 }}> Share }
setShare(false)} r={r} onShared={() => { setShared(true); setShare(false); toast && toast('Recap shared · location data removed', 'green'); }} />
{/* review capture at peak trust — homeowner only, after a good recap */}
{viewer === 'homeowner' && r.photos && (!isPool || (r.chem && !r.chem.handled)) && (
One quick thing
)}
);
}
/* ─── Share sheet — branded, EXIF-stripped shareable card ────────────── */
function ShareRecapSheet({ open, onClose, r, onShared }) {
const isPool = r.svc === 'pool';
return (
A clean, branded card — safe to text family or post anywhere.
{/* the shareable preview */}
AFTER · {r.date.toUpperCase()}
Verified on-site
{isPool ? 'Pool' : 'Garden'} service · {r.dur}
Marta Lawn & Pool · on Mowdew
{isPool && r.chem &&
Balanced}
Location data (EXIF) is stripped — your address never travels with the photo.
Copy link
Save image
Share
);
}
Object.assign(window, { RECAPS, getRecap, BeforeAfter, VisitRecap, ShareRecapSheet });