/* ============================================================================ MOWDEW — Reschedule negotiation screens (shared by web + mobile) NegotiationCard (viewer-aware, every state) · ReschedQueue (owner inbox, desktop + mobile) · HomeReschedStatus (homeowner thread + respond) · NotifScreenM (mobile notifications) · OwnerBellMenu (desktop popover). Load AFTER mowdew-kit, mowdew-owner-data, mowdew-resched. ========================================================================= */ const { useState: rsState } = React; /* local vertical pill (MVert lives in the mobile bundle only) */ function RVert({ vert }) { const v = VERT(vert); return ( {v.name} ); } /* ─── status bars for resolved / waiting states ─────────────────────── */ function ReschedStatusBar({ state, text }) { const m = { approved: { bg: 'var(--green-50)', fg: 'var(--green-800)', icon: 'check-circle' }, declined: { bg: 'var(--alert-50)', fg: 'var(--alert-700)', icon: 'x' }, cancelled: { bg: 'var(--ink-50)', fg: 'var(--ink-600,#6E6B60)', icon: 'x' }, waiting: { bg: 'var(--yellow-50)', fg: 'var(--yellow-700)', icon: 'clock' }, }[state] || { bg: 'var(--ink-50)', fg: 'var(--ink-700)', icon: 'clock' }; return (
{text}
); } /* ============================================================================ NEGOTIATION CARD — the reusable thread card, all states, both viewers ========================================================================= */ function NegotiationCard({ req, viewer = 'owner', onAction, compact = false, hideHeader = false }) { const c = CUSTOMERS.find(x => x.id === req.cust); const w = WORKERS.find(x => x.id === req.worker); const first = c.name.split(' ')[0]; const latest = reqLatestTo(req); const names = viewer === 'owner' ? { owner: 'You', homeowner: first } : { owner: 'Diego', homeowner: 'You' }; const toMeta = req.state === 'approved' ? { label: 'CONFIRMED', tone: 'green' } : req.state === 'declined' ? { label: 'ASKED', tone: 'alert' } : req.state === 'proposed' ? { label: viewer === 'owner' ? 'YOU SUGGESTED' : 'DIEGO SUGGESTED', tone: 'green' } : { label: 'REQUESTED', tone: 'green' }; const btn = (st = {}) => ({ minHeight: compact ? 42 : 44, fontSize: compact ? 13.5 : 14, padding: '10px 14px', ...st }); return (
{/* header */} {!hideHeader && (viewer === 'owner' ? (
{c.name}
{req.rounds > 1 && ROUND {req.rounds}} {c.addr} · {req.age}
) : (
Marta Lawn & Pool
Pool service · Diego decides, you'll hear right here
))} {/* assigned pro — always visible, updates live after a reassign */} {viewer === 'owner' && !hideHeader && (
{w.name}{w.you ? ' · you' : ''} · {w.role}
)} {/* time change */} t.to).to : latest} toLabel={toMeta.label} toTone={toMeta.tone} compact={compact} /> {/* thread */}
{/* actions / status */}
{viewer === 'owner' && req.state === 'pending' && (
)} {viewer === 'owner' && req.state === 'proposed' && ( )} {viewer === 'homeowner' && req.state === 'proposed' && (
)} {viewer === 'homeowner' && req.state === 'pending' && (
)} {req.state === 'approved' && ( viewer === 'homeowner' ? (
) : ( ) )} {req.state === 'declined' && ( viewer === 'homeowner' ? (
t.kind === 'decline') || {}).note || ''}"`} />
) : ( ) )} {req.state === 'cancelled' && ( )}
); } /* ============================================================================ OWNER QUEUE — filterable inbox (desktop + mobile), dialogs included ========================================================================= */ function ReschedQueue({ go, toast, compact = false }) { const [reqs, setReqs] = rsState(() => RESCHED_REQS.map(q => ({ ...q, thread: [...q.thread] }))); const [filter, setFilter] = rsState('needs'); const [dialog, setDialog] = rsState(null); // { type, id } const counts = { needs: reqs.filter(reqNeedsOwner).length, waiting: reqs.filter(reqWaiting).length, done: reqs.filter(reqResolved).length, }; const list = reqs.filter(q => filter === 'needs' ? reqNeedsOwner(q) : filter === 'waiting' ? reqWaiting(q) : reqResolved(q)); const upd = (id, fn) => setReqs(rs => rs.map(q => q.id === id ? fn({ ...q, thread: [...q.thread] }) : q)); const custOf = (id) => CUSTOMERS.find(x => x.id === reqs.find(q => q.id === id).cust); const act = (q, type) => { const c = CUSTOMERS.find(x => x.id === q.cust); const w = WORKERS.find(x => x.id === q.worker); if (type === 'approve') { upd(q.id, x => ({ ...x, state: 'approved', final: reqLatestTo(x), thread: [...x.thread, { by: 'owner', kind: 'approve', to: reqLatestTo(x), at: 'Just now' }] })); toast(`Approved · ${w.name.split(' ')[0]}'s route updated — ${c.name.split(' ')[0]} notified`, 'green'); } else setDialog({ type, id: q.id }); }; const doDecline = (reason) => { const q = reqs.find(x => x.id === dialog.id); const c = custOf(dialog.id); upd(q.id, x => ({ ...x, state: 'declined', thread: [...x.thread, { by: 'owner', kind: 'decline', note: reason, at: 'Just now' }] })); setDialog(null); toast(`Declined — ${c.name.split(' ')[0]} keeps the original time`, 'blue'); }; const doPropose = (time, note) => { const q = reqs.find(x => x.id === dialog.id); const c = custOf(dialog.id); upd(q.id, x => ({ ...x, state: 'proposed', thread: [...x.thread, { by: 'owner', kind: 'propose', to: time, note: note || undefined, at: 'Just now' }] })); setDialog(null); toast(`Suggested ${time} — ${c.name.split(' ')[0]} can accept or counter`, 'green'); }; const doReassign = (wid, alsoApprove) => { const q = reqs.find(x => x.id === dialog.id); const c = custOf(dialog.id); const nw = WORKERS.find(x => x.id === wid); upd(q.id, x => ({ ...x, worker: wid, thread: [...x.thread, { by: 'owner', kind: 'reassign', toWho: nw.name.split(' ')[0], at: 'Just now' }, ...(alsoApprove ? [{ by: 'owner', kind: 'approve', to: reqLatestTo(x), at: 'Just now' }] : [])], ...(alsoApprove ? { state: 'approved', final: reqLatestTo(x) } : {}) })); setDialog(null); toast(alsoApprove ? `Approved & moved to ${nw.name.split(' ')[0]} — routes rebalanced` : `${c.name.split(' ')[0]} reassigned to ${nw.name.split(' ')[0]}`, 'green'); }; const dialogReq = dialog && reqs.find(x => x.id === dialog.id); const dialogCust = dialogReq && CUSTOMERS.find(x => x.id === dialogReq.cust); const dialogTitle = dialog && { decline: `Decline ${dialogCust && dialogCust.name.split(' ')[0]}'s request`, propose: 'Propose a new time', reassign: 'Reassign to another pro', }[dialog.type]; const dialogBody = dialog && ( dialog.type === 'decline' ? : dialog.type === 'propose' ? : ); return (
{/* filter chips */}
{[['needs', 'Needs you'], ['waiting', 'Waiting'], ['done', 'History']].map(([id, lb]) => { const on = filter === id; return ( ); })}
{list.map(q => act(q, t)} />)} {list.length === 0 && (
{filter === 'needs' ? 'Nothing needs you.' : filter === 'waiting' ? 'Nothing in flight.' : 'No resolved requests yet.'}
{filter === 'needs' ? 'New reschedule requests land here first.' : filter === 'waiting' ? 'Suggestions you send wait here for an answer.' : 'Approved, declined and cancelled requests file here.'}
)}
{/* dialogs — Sheet on mobile, Modal on web */} {compact ? ( setDialog(null)} title={dialogTitle}>{dialogBody} ) : ( setDialog(null)} title={dialogTitle}>{dialogBody} )}
); } /* ============================================================================ HOMEOWNER — negotiation status / thread screen (h_resched) ========================================================================= */ function HomeReschedStatus({ go, toast, params }) { const fresh = params && params.fresh; const [req, setReq] = rsState(() => fresh ? { id: 'qx', cust: 'c2', worker: 'w1', state: 'pending', age: 'Just now', orig: 'Mon, May 18 · 10 AM', thread: [{ by: 'homeowner', kind: 'request', to: params.to, note: params.note || undefined, at: 'Just now' }] } : (() => { const q = RESCHED_REQS[0]; return { ...q, thread: [...q.thread] }; })()); const [sheet, setSheet] = rsState(null); // 'counter' | 'decline' const latest = reqLatestTo(req); const turn = (kind, extra = {}) => ({ by: 'homeowner', kind, at: 'Just now', ...extra }); const onAction = (type) => { if (type === 'accept') { setReq(r => ({ ...r, state: 'approved', final: latest, thread: [...r.thread, { by: 'owner', kind: 'approve', to: latest, at: 'Just now' }] })); toast(`Confirmed — see you ${latest}`, 'green'); } else if (type === 'counter') setSheet('counter'); else if (type === 'declineProposal') setSheet('decline'); else if (type === 'cancel') { setReq(r => ({ ...r, state: 'cancelled', thread: [...r.thread, turn('cancel', { note: 'Cancelled from the app.' })] })); toast('Request cancelled — your original time stands', 'blue'); } else if (type === 'calendar') toast('Added to your calendar', 'green'); else if (type === 'message') go('h_request'); }; const doCounter = (time, note) => { setReq(r => ({ ...r, state: 'pending', rounds: (r.rounds || 1) + 1, thread: [...r.thread, turn('counter', { to: time, note: note || undefined })] })); setSheet(null); toast(`Sent — Diego will respond to ${time}`, 'green'); }; const doDeclineProposal = (reason) => { setReq(r => ({ ...r, state: 'declined', thread: [...r.thread, turn('decline', { note: reason })] })); setSheet(null); toast('Okay — your original time stands', 'blue'); }; return ( <> go('h_feed')} />
{req.state === 'approved' && (
All set
Your pool visit moved to {req.final}.
Diego's route is updated — no need to do anything else.
)}
Every change lands on the route automatically — nothing to text about.
setSheet(null)} title="Propose another time"> setSheet(null)} title="Decline Diego's suggestion"> ); } /* ============================================================================ NOTIFICATIONS — mobile screen (all personas) + desktop bell popover ========================================================================= */ function NotifScreenM({ persona, go, toast }) { const back = persona === 'owner' ? 'o_home' : persona === 'pro' ? 'pro_today' : 'h_feed'; const openLink = (n) => { const map = persona === 'owner' ? { resched: ['o_inbox', { tab: 'approvals' }], billing: ['o_billing_run', {}] } : persona === 'homeowner' ? { resched: ['h_resched', {}], invoice: ['h_inbox', {}], feed: ['h_feed', {}] } : { today: ['pro_today', {}] }; const t = map[n.link]; if (t) go(t[0], t[1]); }; return ( go(back)} />
); } /* desktop owner — bell + anchored popover in the shell header */ function OwnerBellMenu({ go }) { const [open, setOpen] = rsState(false); const [, force] = rsState(0); const count = notifUnreadCount('owner'); const openLink = (n) => { setOpen(false); force(x => x + 1); go(n.link === 'billing' ? 'billingrun' : 'reschedules'); }; return (
setOpen(o => !o)} /> {open && ( <>
setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 70 }} />
)}
); } Object.assign(window, { RVert, ReschedStatusBar, NegotiationCard, ReschedQueue, HomeReschedStatus, NotifScreenM, OwnerBellMenu });