"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.
) : (
/* 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. */
{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}
/>
)}
>
);
}