"use client"; /* Reviewing one request: approve it, or knock back the garments that shouldn't go. * * One request covers everything the person asked for, so this screen shows the whole ask and * settles it in one action. A manager can still refuse part of it — the tunic and the trousers * yes, the fleece no — and each line carries its own reasons, in the list, where the garment is. * Everything not knocked back is approved when the bar is pressed, and the bar says how many that * is, because "Approve" over a list of three with one struck out has to be unambiguous. * * The decline reason is compulsory and comes from a fixed list of three, and it is always shown to * the staff member — per garment now, rather than for the request as a whole. That is the point: * the thing this replaces is a request that goes quiet, and a refusal nobody can explain is the * same failure with an extra step. * * A manager can be the person the request is for. Two ward managers commonly name each other as * approver — that is how the top of the tree gets one at all — and the server lets the wearer * settle their own. The queue sets those apart; so does the line above the bar here, because * having been told on a list you scrolled past is not the same as being told as you sign. * * Not inside the Team shell: this is a detail screen with a back chevron, like the order and the * thread. A tab row on a decision screen invites somebody off it mid-decision. */ import { useState } from "react"; import { useRouter } from "next/navigation"; import { MBar, MBody, MError, MRule, MTop } from "@/components/m"; import { ACCENT_700, IdentityBlock, INK, Kicker, LineList, N600, N700, lineText } from "@/components/staffui"; import { Band, ChipAction } from "./Team"; import { useStaff } from "@/lib/staffclient"; import { DECLINE_REASONS } from "@/lib/staffreq"; import type { ReviewLine } from "@/lib/managerdata"; type Data = { id: string; code: string; status: string; subject: { id: string; name: string; num: string; group: string; ward: string; held: number; sets: number; approvedThisYear: number }; lines: ReviewLine[]; summary: string; garments: number; lineCount: number; decision: string | null; reason: string; note: string; raisedByName: string; allowance: { capped: boolean; label: string; note: string; over: boolean }; }; /** What the manager has pencilled against each line before they press the bar. */ type Call = { decision: "approved" | "declined"; reason: string }; /** The shelf word, in a box beside the garment. Words, never a count. */ function StockBox({ word, struck }: { word: string; struck: boolean }) { const none = word.startsWith("none"); return ( {word} ); } export default function ReviewScreen({ data }: { data: Data }) { const router = useRouter(); const { me, mutate, busy } = useStaff(); /* Everything starts approved. That is not a default in the lazy sense — it is what the button at * the bottom will do, spelled out on every line before it is pressed, so the manager is choosing * what to refuse rather than ticking off what to allow. */ const [calls, setCalls] = useState>( () => Object.fromEntries(data.lines.map((l) => [l.id, { decision: "approved" as const, reason: "" }])), ); const [err, setErr] = useState(""); const decided = data.status !== "awaiting"; /* Whether the person deciding this is the person it is for. Matched on the id, which the register * keeps unique; a name would start calling a stranger's request yours the day two people on the * ward share one, and what is being marked here is an audit fact. */ const mine = data.subject.id === me.staffId; const yes = data.lines.filter((l) => calls[l.id]?.decision === "approved").length; const total = data.lines.length; function decline(lineId: string, reason: string) { setErr(""); setCalls((c) => ({ ...c, [lineId]: { decision: "declined", reason } })); } async function send() { const lines = data.lines.map((l) => ({ id: l.id, decision: calls[l.id]?.decision ?? "approved", reason: calls[l.id]?.reason ?? "", })); const every = lines.every((l) => l.decision === "declined"); // The op name matches the outcome the manager can see on the button; `lines` is what actually // decides, garment by garment, and it has to name every one of them exactly once. const r = await mutate(every ? "request.decline" : "request.approve", { id: data.id, lines, // When the whole request went, and every line went for the same reason, that reason is the // request's reason too — it is what the wearer's order and the decision email lead with. reason: every && new Set(lines.map((l) => l.reason)).size === 1 ? lines[0].reason : undefined, }); if (!r.ok) { setErr(r.error); return; } router.push("/my/approvals"); } const barLabel = busy ? "Working…" : yes === 0 ? (total === 1 ? "Decline" : `Decline all ${total}`) : yes === total ? (total === 1 ? "Approve" : `Approve all ${total}`) : `Approve ${yes} of ${total}`; return ( <> {/* A real destination, not history.back(): this screen opens cold from the approval email. */} router.push("/my/approvals")} /> {/* No count at the right. Every garment is listed under this band with its own quantity, and a total beside the heading only invites the manager to decide against the number rather than against the list. */} {decided ? ( <>

{data.decision ? `${data.decision}. ` : ""}Nothing here is waiting on you.

) : (
{data.lines.map((l) => { const call = calls[l.id] ?? { decision: "approved" as const, reason: "" }; const off = call.decision === "declined"; return (
{lineText(l)}
{off ? (
{call.reason}
) : ( /* The reasons are the control. A Decline button that opens them was a tap that told nobody anything, and the three of them fit on the line they belong to. */
{DECLINE_REASONS.map((r) => ( decline(l.id, r)} /> ))}
)}
); })}
)} {(data.reason || data.note || data.raisedByName) && (
Why they asked
{[data.reason, data.note].filter(Boolean).join(" — ")}
{data.raisedByName && (
Raised for them by {data.raisedByName}.
)}
)}
Allowance
{data.allowance.label}

{data.allowance.note}

{mine && !decided && ( /* Last thing above the bar, because the bar is what does it. The reader finds out here rather than from an auditor months later. */

Approving it is recorded as your own approval.

)} setErr("")} />
{!decided && ( /* `small`, not `sub`: the count of what is about to be refused is mono at the right of the bar, reading against the outcome rather than as a subtitle underneath it. */ 0 ? "check" : "arrow"} tone={yes > 0 ? "accent" : "ink"} disabled={busy} onClick={send} /> )} ); }