"use client"; /* The Work tab: the queue at the window. Approved requests to pick, deliveries to receive, pickups to call and ward rounds, counted by the same useWorkCount() as the tab badge and Today. */ import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { useDerived, useSnap } from "@/lib/client"; import { collectRows, plural, receiveRows, relativeDay, roundSheet } from "@/lib/today"; import { PICK_STATUSES, useWorkCount } from "@/lib/workcount"; import { useRequests } from "@/components/requests/RequestList"; import { MBody, MButton, MCard, MCardChip, MEmpty, MError, MKick, MPill, MRow, MRule, MSection, MSeg, MTabs, MTop, MTopCount, useToast, } from "@/components/m"; import { openReceivable, orderWhen, outstandingTotal } from "@/components/m/work/util"; type Seg = "picks" | "in" | "pickups" | "rounds"; const SEGS: readonly Seg[] = ["picks", "in", "pickups", "rounds"]; const asSeg = (v: string | null): Seg => (SEGS.includes(v as Seg) ? (v as Seg) : "picks"); export default function Work() { const sp = useSearchParams(); const [seg, setSeg] = useState(() => asSeg(sp.get("seg"))); const w = useWorkCount(); const [err, setErr] = useState(""); // A link from Today (?seg=in) lands on its segment even when Work is already mounted. useEffect(() => { setSeg(asSeg(sp.get("seg"))); }, [sp]); const pick = (k: Seg) => { setSeg(k); setErr(""); // Shallow: the segment is only a view of the snapshot already on the phone. null state, so Next's // router takes the new URL as canonical and a later refresh doesn't put the old segment back. try { window.history.replaceState(null, "", `/m/work?seg=${k}`); } catch { /* not fatal */ } }; return ( <> {w.total} open} /> setErr("")} /> {seg === "picks" && } {seg === "in" && } {seg === "pickups" && } {seg === "rounds" && } ); } const line = (text: string) => {text}; function Picks() { const { s } = useSnap(); const { staffById } = useDerived(); const { data, error, reload } = useRequests(); const rows = useMemo(() => (data?.requests ?? []) .filter((r) => PICK_STATUSES.has(r.status)) .sort((a, b) => (a.decidedAt || a.createdAt).localeCompare(b.decidedAt || b.createdAt)), [data]); if (!data) { if (error) return (
void reload()} />
); return
Loading
; } if (!rows.length) return ; return ( <> {rows.map((r) => { const st = staffById[r.staffId]; const what = r.bag.map((l) => `${l.item.toLowerCase()} ${l.size}`).join(", "); const when = relativeDay(r.decidedAt, s); const approved = [when ? `Approved ${when}` : "Approved", r.managerName].filter(Boolean).join(" · ") + (r.status === "ready" && r.collectCode ? ` · ready, code ${r.collectCode}` : ""); return ( {line([st?.group || r.ward, what].filter(Boolean).join(" · "))}{line(approved)}} right={r.garments} /> ); })} ); } function Inbound() { const { s } = useSnap(); const { byId, staffById } = useDerived(); const soon = useMemo(() => receiveRows(s, staffById), [s, staffById]); const later = useMemo(() => { const ids = new Set(soon.map((r) => r.o.id)); return openReceivable(s, byId).filter((o) => !ids.has(o.id)); }, [s, byId, soon]); const row = (o: (typeof later)[number], forName: string) => ( ); if (!soon.length && !later.length) return ; return ( <> {soon.map((r) => row(r.o, r.forName))} {later.length > 0 && ( <> {later.map((o) => row(o, o.staffId ? (staffById[o.staffId] ? `${staffById[o.staffId].first} ${staffById[o.staffId].last}`.trim() : "") : ""))} )} ); } function Pickups({ onError }: { onError: (msg: string) => void }) { const { s, mutate } = useSnap(); const { byId, staffById } = useDerived(); const toast = useToast(); const rows = useMemo(() => collectRows(s, byId, staffById, { includeRound: true }), [s, byId, staffById]); const [busy, setBusy] = useState(null); const run = async (id: string, op: string, payload: Record, then?: () => void) => { if (busy) return; setBusy(id); onError(""); const r = await mutate(op, payload); setBusy(null); if (!r.ok) { onError(r.error); return; } then?.(); }; if (!rows.length) return ; return ( <> {rows.map((r) => { const items = r.lines.map((l) => `${l.garment} ${l.size} ×${l.qty}`).join(", "); const id = r.p.id; return ( {r.days}d} actions={ <> void run(id, "pickup.contacted", { id, contacted: !r.p.contacted })} /> void run(id, "pickup.pickedUp", { id }, () => toast(`${r.name} collected ${items}`))} /> } /> ); })} ); } function Rounds() { const { s } = useSnap(); const { byId, staffById } = useDerived(); const wards = useMemo(() => roundSheet(s, byId, staffById), [s, byId, staffById]); if (!wards.length) return ; return ( <> {wards.map((w) => { const people = new Set(w.rows.map((r) => r.p.staffId)).size; return ( ); })} ); }