"use client"; /* 1E — New request. One ask, however many garments it takes. * * A nurse who needs a tunic, trousers and a fleece used to raise three requests: three codes, * three emails to the same manager on the same morning, three bags to collect. So this screen is * built around a list the person is filling rather than a single garment — add a line, add * another, one reason and one note over the lot, one approval at the end. * * Two things carried over from the old screen because they do most of the work. Availability is * shown **before** the request is sent, so nobody asks for a size that isn't there and then waits * a week to find out. And the button names the actual approver — "Send to D. Adeyemi", not * "Submit" — because the single most common question about a request is who has it. * * What is new is that the screen also shows what the person already holds and what they are * allowed, from the same sum the manager's review screen uses. Being declined "Over allowance" * against a number you were never shown is the sort of refusal that ends in a phone call. * * No prices, no payment, no basket. A request covering four garments is not a basket; approval is * still the only control. */ import { useState } from "react"; import { MBar, MBody, MError, MRule, MTop } from "@/components/m"; import { DarkCard, DraftLineList, GarmentPicker, Kicker, N500, N600, N700, NumberedField, OptionList, StockTag, type DraftLine, } from "@/components/staffui"; import { useStaff } from "@/lib/staffclient"; import { REQUEST_REASONS } from "@/lib/staffreq"; import { fmtDate } from "@/lib/compute"; type Size = { size: string; si: number; word: "in_stock" | "low" | "none" | string; countedOn: string; held: number }; type Item = { id: string; item: string; type: string; gender: string; sizes: Size[]; recorded: string; recordedSource: "record" | "issued" | ""; held: number; }; type Allowance = { capped: boolean; label: string; note: string; over: boolean }; export default function RequestScreen({ items, managerName, swap, heldItemIds, preItemId, preSi, holding, allowance, maxLines, maxQty, }: { items: Item[]; managerName: string; swap: boolean; heldItemIds: string[]; preItemId: string | null; preSi: number | null; holding: { total: number; sets: number }; allowance: Allowance; maxLines: number; maxQty: number; }) { const { mutate, busy } = useStaff(); // Swapping a size is a request against something you already hold, so the list is the shorter // one. Everything else about the screen is identical — a swap is not a different kind of ask. const list = swap && heldItemIds.length ? items.filter((i) => heldItemIds.includes(i.id)) : items; /* Arriving from the waitlist's "or take a stocked size", the garment and size are already * decided — the person picked them on the previous screen and should not have to again. So the * list starts with that line already on it rather than with an empty picker. */ const [lines, setLines] = useState(() => { const it = preItemId ? list.find((i) => i.id === preItemId) : null; const s = it && preSi !== null ? it.sizes.find((x) => x.si === preSi) : null; return it && s ? [{ key: "pre", itemId: it.id, si: s.si, item: it.item, size: String(s.size), qty: 1 }] : []; }); const [adding, setAdding] = useState(false); const [reason, setReason] = useState(null); const [note, setNote] = useState(swap ? "Swapping a size." : ""); const [err, setErr] = useState(""); const [sent, setSent] = useState<{ id: string; manager: string } | null>(null); const garments = lines.reduce((n, l) => n + l.qty, 0); const full = lines.length >= maxLines; // With nothing on the list there is nothing to show but the picker, so it opens itself. const picking = adding || lines.length === 0; /* The same garment in the same size, added twice, is one line of two rather than two lines of * one. The server sums duplicates anyway before it writes them, so a screen that showed two * identical rows would be showing something that cannot be saved. */ function add(l: { itemId: string; si: number; item: string; size: string; qty: number }) { setErr(""); setLines((cur) => { const at = cur.findIndex((x) => x.itemId === l.itemId && x.si === l.si); if (at >= 0) { const next = [...cur]; next[at] = { ...next[at], qty: Math.min(maxQty, next[at].qty + l.qty) }; return next; } return [...cur, { ...l, key: `${l.itemId}:${l.si}:${cur.length}` }]; }); setAdding(false); } /* Raised, but nobody has been emailed. * * Not an error — the request is on the manager's list either way — but the order screen it would * otherwise jump straight to says "Awaiting approval" and nothing else, and somebody who believes * there is a message sitting in their manager's inbox will wait a fortnight before chasing it. * So the one case where no email left the server is said plainly, once, with the thing to do. */ if (sent) { return ( <>
Ask the linen room to give {sent.manager || "your manager"} a code for the staff app, or mention it to them yourself.
); } return ( <> {list.length === 0 ? (

{swap ? "Nothing on your record to swap. Ask for an item instead." : "The linen room hasn’t listed any garments yet."}

) : ( <> {lines.length > 0 && (
setLines((cur) => cur.map((l) => (l.key === k ? { ...l, qty: q } : l)))} onRemove={(k) => setLines((cur) => cur.filter((l) => l.key !== k))} />
)} {picking ? ( setAdding(false) : undefined} // The size the record already knows — their recorded top or trouser size, or the // size of the last one they were issued for everything else. defaultSi={(it) => it.sizes.find((s) => String(s.size) === String(it.recorded))?.si ?? null} note={(it, s) => ( <> {it.recordedSource === "record" ? `Your recorded size is ${it.recorded}. ` : ""} {it.recordedSource === "issued" ? `Last issued in ${it.recorded}. ` : ""} {it.held > 0 ? `You hold ${it.held}. ` : ""} {s ? ( <> {s.word === "none" && " — the linen room will order it in"} {s.countedOn ? ` · counted ${fmtDate(s.countedOn)}` : ""} ) : "Pick a size."} )} onAdd={add} /> ) : full ? (

That is {maxLines} lines — as much as one request carries. Send this one and raise another for anything else.

) : ( )} )}
{/* What they hold and what they are entitled to, in the manager's own words. Shown while they are still choosing rather than in the decline. */}
What you hold
{holding.total === 0 ? "Nothing on your record yet." : `${holding.total} garment${holding.total === 1 ? "" : "s"}${holding.sets ? ` · ${holding.sets} set${holding.sets === 1 ? "" : "s"}` : ""}`}
{allowance.label}

{allowance.note}

{lines.length > 0 && ( { setReason(k); setErr(""); }} options={REQUEST_REASONS.map((r) => ({ key: r, label: r }))} />