"use client"; /* Team ▸ Round — what the desk sees when the trolley arrives. * * Unclaimed bags from previous rounds sit **above** today's work. They are the linen room's * biggest waste — a bag signed for on the ward and never collected is a garment out of stock and * off the record — so the screen refuses to bury them under whatever arrived this morning. * * Anyone on the ward can sign, and whoever does is named on the requester's order. That is the * whole audit story: a missing bag has a name against it. * * Signing for the lot now reports itself. It used to loop silently: on a ward where the fourth bag * was refused, the clerk saw an unchanged list and no idea which three had gone through, so the * honest thing — a count that climbs, and the name of whatever stopped it — is on the screen. */ import { useState } from "react"; import { MEmpty, MError } from "@/components/m"; import { ACCENT_300, DoneRow, EdgeRow, GROUND, INK, Kicker, N300, N600, Progress, SecondaryBar, lineText, } from "@/components/staffui"; import Team, { Band, ChipAction, ChipRow } from "./Team"; import { useStaff } from "@/lib/staffclient"; import { statusText } from "@/lib/staffreq"; import type { ReqLine, ReqRow } from "@/lib/staffdata"; import { formatInZone } from "@/lib/compute"; type Bag = { id: string; code: string; subjectName: string; /** What is actually in the bag: the approved lines, and nothing the manager knocked back. */ lines: ReqLine[]; summary: string; garments: number; lineCount: number; status: string; signerName: string | null; signedAt: string | null; claimedAt: string | null; since: string; }; export default function RoundScreen({ ward, toSign, unclaimed, signedToday, raised }: { ward: string; toSign: Bag[]; unclaimed: Bag[]; signedToday: Bag[]; /** Open requests this person raised for someone else, however they came to raise them. */ raised: ReqRow[]; }) { const { mutate, busy, me } = useStaff(); /* `signedAt` arrives as a UTC instant, and both of the places it is shown used to read it as * local text. The ward round happens in the morning, which is exactly when the UTC date is still * yesterday's, so the desk was routinely told a bag it signed for an hour ago went out the day * before. The facility's zone answers both. */ const tz = me.tz; const [err, setErr] = useState(""); const [prog, setProg] = useState<{ done: number; total: number } | null>(null); async function sign(id: string) { setErr(""); const r = await mutate("round.sign", { id }); if (!r.ok) setErr(r.error); } async function claim(id: string) { setErr(""); const r = await mutate("round.claim", { id }); if (!r.ok) setErr(r.error); } /* One call per bag, because that is what signing is — a signature against one hand-over — and the * count climbs after each. It stops at the first refusal and says which bag it stopped on: the * ones already signed are done and must not be sent again (round.sign is not idempotent), and * the rest are left exactly as they were for the clerk to deal with. */ async function signAll() { const list = toSign; if (!list.length) return; setErr(""); setProg({ done: 0, total: list.length }); let done = 0; for (const b of list) { const r = await mutate("round.sign", { id: b.id }); if (!r.ok) { setErr(`Signed ${done} of ${list.length} — ${b.subjectName}: ${r.error}`); setProg(null); return; } done += 1; setProg({ done, total: list.length }); } setProg(null); } const nothingAtAll = toSign.length === 0 && signedToday.length === 0 && unclaimed.length === 0; return ( 1 ? (
{/* Bulk signing is allowed but not encouraged — a secondary action, never the red one. */} void signAll()} />
) : undefined} >
Ward round
{toSign.length}
{toSign.length === 1 ? "bag to sign" : "bags to sign"}
{toSign.length === 0 ? "nothing waiting" : "arriving today"}
{signedToday.length > 0 && (
{signedToday.length} signed
)}
{prog && } setErr("")} /> {unclaimed.length > 0 && ( <>
{unclaimed.map((b) => (
{b.subjectName}
{[b.lines.map(lineText).join(", "), b.signedAt ? `since ${formatInZone(b.signedAt, tz, { weekday: "long" })}` : ""].filter(Boolean).join(" · ")}
{/* Nudging only works if the requester eventually opens the app; often the bag went days ago and the person who knows that is the clerk standing where it used to be, which is what Collected is for. */} void claim(b.id)} />
))}
)} {/* The count stays on the band when it reaches nought: a heading that drops its note the moment the work is done reads as a screen that has lost its place. */} {nothingAtAll ? (
) : ( <> {toSign.map((b) => (
{b.subjectName}
{/* Signing is a signature: whoever puts their name to a bag of four garments should be able to read the four before they do, rather than a count of them and a code. That is why the garments themselves are the row's second line. Nothing declined is listed — a knocked-back garment never reaches the trolley. */}
{b.lines.map(lineText).join(", ")}
void sign(b.id)} />
))} {signedToday.map((b) => (
{b.subjectName}
Signed {b.signedAt ? formatInZone(b.signedAt, tz, { hour: "2-digit", minute: "2-digit", hour12: false }) : ""} by {b.signerName}
Done
))} {/* Under the signed rows rather than instead of them: the desk wants this morning's work still on the screen AND to be told it is finished. Drawn whenever nothing is left to sign, which is what the mockup does once the last bag has a name against it. */} {toSign.length === 0 && (
)} )} {/* What this person raised in somebody else's name, still on its way. The bags above are only the ones arriving today; a request typed in on Tuesday and approved on Thursday is invisible there until it turns up on a trolley, so without this the only way to find out where it had got to was to ring the linen room. */} {raised.length > 0 && ( <>
{raised.map((r) => { const st = statusText(r, { mine: false, first: r.subjectName?.split(" ")[0] }); return (
{r.subjectName} {r.code}
{r.summary}
{[st.label, st.note].filter(Boolean).join(" · ")}
); })}
)}
); }