/* ============================================================================ MOWDEW — Reschedule negotiation kit (shared) The negotiation model (pending → proposed ⇄ counter → approved / declined / cancelled) + the atoms it's built from: state chips, time-change block, mini calendar + time picker, decline / propose / reassign forms, the append-only thread, a desktop Modal, and the in-app notification system (data + bell + feed rows) for all three personas. Load AFTER mowdew-kit.jsx (+ mowdew-owner-data.jsx where CUSTOMERS is used). Screens that compose these live in mowdew-resched-screens.jsx. ========================================================================= */ /* ─── Requests — one coherent May 2026 world (today = Tue, May 12) ────── thread turns: { by: 'homeowner'|'owner', kind: 'request'|'propose'| 'counter'|'approve'|'decline'|'cancel', to?, note?, at } */ const RESCHED_REQS = [ { id: 'q1', cust: 'c2', worker: 'w1', state: 'proposed', age: '2h ago', orig: 'Mon, May 18 · 10 AM', thread: [ { by: 'homeowner', kind: 'request', to: 'Thu, May 21 · morning', note: 'Pool party Wednesday — could Diego come Thursday instead?', at: 'Tue 9:15 AM' }, { by: 'owner', kind: 'propose', to: 'Sat, May 23 · 9 AM', note: "Thursday's full — would Saturday morning work?", at: 'Tue 11:02 AM' }, ] }, { id: 'q2', cust: 'c5', worker: 'w2', state: 'pending', age: '5h ago', orig: 'Wed, May 20 · 1 PM', thread: [ { by: 'homeowner', kind: 'request', to: 'Fri, May 22 · 1 PM', note: 'Gate will be locked Wednesday.', at: 'Mon 6:30 PM' }, ] }, { id: 'q3', cust: 'c3', worker: 'w2', state: 'pending', age: '1d ago', rounds: 2, orig: 'Mon, May 18 · 8:30 AM', thread: [ { by: 'homeowner', kind: 'request', to: 'Thu, May 21 · 8:30 AM', note: "We're repaving the driveway Monday.", at: 'Sun 4:10 PM' }, { by: 'owner', kind: 'propose', to: 'Sat, May 23 · 8:30 AM', note: 'Marco is booked Thursday — Saturday instead?', at: 'Sun 6:45 PM' }, { by: 'homeowner', kind: 'counter', to: 'Sun, May 24 · afternoon', note: "We're out Saturday — Sunday afternoon?", at: 'Mon 8:20 AM' }, ] }, { id: 'q4', cust: 'c6', worker: 'w1', state: 'approved', age: '4d ago', orig: 'Tue, May 12 · 9 AM', final: 'Fri, May 15 · 2 PM', thread: [ { by: 'homeowner', kind: 'request', to: 'Fri, May 15 · 2 PM', note: 'Hosting a brunch Tuesday morning.', at: 'Fri 2:12 PM' }, { by: 'owner', kind: 'approve', to: 'Fri, May 15 · 2 PM', at: 'Fri 3:05 PM' }, ] }, { id: 'q5', cust: 'c8', worker: 'w1', state: 'declined', age: '5d ago', orig: 'Wed, May 13 · 11 AM', thread: [ { by: 'homeowner', kind: 'request', to: 'Thu, May 14 · 11 AM', note: 'Painters are coming Wednesday.', at: 'Thu 7:40 PM' }, { by: 'owner', kind: 'decline', note: 'Too short notice — that day was already dispatched. Kept your Wednesday slot.', at: 'Thu 8:15 PM' }, ] }, { id: 'q6', cust: 'c1', worker: 'w1', state: 'cancelled', age: '1w ago', orig: 'Mon, May 11 · 9 AM', thread: [ { by: 'homeowner', kind: 'request', to: 'Wed, May 13 · 9 AM', note: 'We might be travelling.', at: 'Mon 8:00 AM' }, { by: 'homeowner', kind: 'cancel', note: 'Never mind — plans changed, Monday works.', at: 'Mon 9:30 AM' }, ] }, ]; function reqLatestTo(q) { const t = [...q.thread].reverse().find(x => x.to); return t ? t.to : q.orig; } function reqNeedsOwner(q) { return q.state === 'pending'; } function reqWaiting(q) { return q.state === 'proposed'; } function reqResolved(q) { return q.state === 'approved' || q.state === 'declined' || q.state === 'cancelled'; } function needsOwnerCount() { return RESCHED_REQS.filter(reqNeedsOwner).length; } /* ─── State chip — viewer-aware labels ──────────────────────────────── */ const RESCHED_CHIP = { pending: { tone: 'soon', owner: 'Needs you', homeowner: 'Pending', ownerShort: 'Needs you', homeShort: 'Pending' }, proposed: { tone: 'soon', owner: 'Waiting on customer', homeowner: 'New time suggested', ownerShort: 'Waiting', homeShort: 'New time' }, approved: { tone: 'done', owner: 'Confirmed', homeowner: 'Confirmed', icon: 'check', ownerShort: 'Confirmed', homeShort: 'Confirmed' }, declined: { tone: 'alert', owner: 'Declined', homeowner: 'Declined', ownerShort: 'Declined', homeShort: 'Declined' }, cancelled: { tone: 'neutral', owner: 'Cancelled', homeowner: 'Cancelled', ownerShort: 'Cancelled', homeShort: 'Cancelled' }, }; function ReschedChip({ state, viewer = 'owner', compact = false }) { const m = RESCHED_CHIP[state] || RESCHED_CHIP.pending; const label = compact ? (viewer === 'owner' ? m.ownerShort : m.homeShort) : m[viewer]; return {label}; } /* ─── Time change — FROM → TO, the calendar handshake ───────────────── */ function TimeChange({ from, to, toLabel = 'TO', toTone = 'green', compact = false }) { const toColor = toTone === 'alert' ? 'var(--alert-700)' : toTone === 'aqua' ? 'var(--aqua-700)' : 'var(--green-700)'; return (
FROM
{from}
{toLabel}
{to}
); } /* ─── Mini calendar — May / Jun 2026, real weekday layout ───────────── */ const CAL_MONTHS = [ { label: 'May 2026', days: 31, startCol: 5, today: 12 }, // May 1, 2026 = Friday { label: 'June 2026', days: 30, startCol: 1 }, // Jun 1, 2026 = Monday ]; function MiniCalendar({ value, onChange }) { const [mi, setMi] = React.useState(0); const m = CAL_MONTHS[mi]; const pick = (d) => onChange(`${m.label.split(' ')[0]} ${d}`); const selected = (d) => value === `${m.label.split(' ')[0]} ${d}`; const disabled = (d) => mi === 0 && m.today && d <= m.today; return (
{m.label}
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => ( {d} ))}
{Array.from({ length: m.startCol }).map((_, i) => )} {Array.from({ length: m.days }).map((_, i) => { const d = i + 1; const on = selected(d); const off = disabled(d); const isToday = mi === 0 && d === m.today; return ( ); })}
); } /* ─── Time chips ────────────────────────────────────────────────────── */ const TIME_SLOTS = ['8 AM', '9 AM', '10 AM', '11 AM', '1 PM', '2 PM', '4 PM']; function TimeChips({ value, onChange }) { return (
{TIME_SLOTS.map(t => { const on = value === t; return ( ); })}
); } /* ─── Propose / counter form — calendar + time + note ───────────────── */ function ProposeForm({ submitLabel = 'Propose this time', notePlaceholder = 'Add a note (optional)…', onSubmit }) { const [date, setDate] = React.useState(null); const [time, setTime] = React.useState(null); const [note, setNote] = React.useState(''); return (