"use client"; /* 2B — Waitlist. What "none on the shelf" leads to instead of a dead end. * * Position is shown **before** joining, because "you are fourth" and "you are fortieth" are * different decisions and only one of them is worth waiting for. The nearest stocked sizes sit * right underneath for the same reason: most people would rather have something that fits * approximately today than exactly in three weeks, and the screen should let them say so. * * Joining needs no approval — a queue is not a request. Approval happens if and when the item * lands and they accept it. */ import { useState } from "react"; import { MBar, MBody, MError, MRule, MTop } from "@/components/m"; import { ACCENT_300, CompactAction, DarkCard, DIVIDER, GROUND, INK, Kicker, N300, N600, N700, StockTag } from "@/components/staffui"; import { useStaff } from "@/lib/staffclient"; import { fmtDate, formatInZone } from "@/lib/compute"; import { WAITLIST_HOLD_HOURS } from "@/lib/staffreq"; type Alt = { si: number; size: string; word: string }; type Data = { itemId: string; item: string; size: string; si: number; lastRestocked: string; position: number; ahead: number; joined: boolean; entryId: string | null; offeredAt: string | null; holdUntil: string | null; offerExpired: boolean; acceptedAt: string | null; alternatives: Alt[]; }; export default function WaitlistScreen({ data }: { data: Data }) { const { mutate, busy, me } = useStaff(); const [err, setErr] = useState(""); const [joined, setJoined] = useState(data.joined); /* Four states, and the screen used to know about two of them. * * An offer is not a standing invitation: it has been accepted, or it is live, or the 48-hour * hold has run out. Keying only off `offeredAt` meant somebody who had already accepted was * shown "Accept it" again — every tap answered "Nothing to accept" — and somebody whose hold * had lapsed was shown a bar the server now refuses, with no word about why. */ const acceptedAt = data.acceptedAt; const accepted = !!acceptedAt; const heldForMe = !accepted && !!data.offeredAt && !data.offerExpired; const lapsed = !accepted && !!data.offeredAt && data.offerExpired; /* The deadline, with the time on it, in the facility's zone. * * `holdUntil` is an instant 48 hours after the offer, and it was being shown by slicing the first * ten characters of the UTC string — so an offer made at 09:00 Brisbane printed the previous day's * date, and a nurse reading "held until the 9th" had until the 10th. The hour matters as much as * the date here: a hold that runs out mid-afternoon is not the same as one that runs to midnight. */ const heldUntil = data.holdUntil ? formatInZone(data.holdUntil, me.tz, { day: "numeric", month: "short", hour: "numeric", minute: "2-digit" }) : ""; return ( <>
None on the shelf
{data.item} — {data.size}

{data.lastRestocked ? `Last counted ${fmtDate(data.lastRestocked)}. ` : ""} The linen room orders these in when the shelf runs down.

setErr("")} />
{accepted ? (
Accepted {formatInZone(acceptedAt || "", me.tz)}. Follow it on your orders.
) : heldForMe ? (
{heldUntil ? `Held until ${heldUntil}.` : "Held for you."}
) : (
{joined ? "You’re on the list" : "If you join"}
{ordinal(data.position)}
in queue
{data.ahead === 0 ? "nobody ahead of you" : `${data.ahead} ${data.ahead === 1 ? "person" : "people"} already waiting`}
{lapsed ? `One came in and was held for you until ${heldUntil || "the hold ran out"}. Nobody took it, so the hold has run out — you’re still on the list, and the linen room can offer it again.` : `You’ll get a message the day it lands, and the item is held for you for ${WAITLIST_HOLD_HOURS} hours.`}
)}
{data.alternatives.length > 0 && !heldForMe && !accepted && ( <>
Or take a stocked size
{data.alternatives.map((a) => (
{data.item} — {a.size}
{ window.location.assign(`/my/request?item=${data.itemId}&si=${a.si}`); }} />
))} )}

Waiting doesn’t need approval. Your manager only sees it if the item comes in and you accept it.

{accepted ? null : heldForMe ? ( { const r = await mutate<{ request: { id: string } }>("waitlist.accept", { id: data.entryId }); if (!r.ok) { setErr(r.error); return; } window.location.assign(`/my/orders/${r.result.request.id}`); }} /> ) : joined ? ( { const r = await mutate("waitlist.leave", { id: data.entryId }); if (!r.ok) { setErr(r.error); return; } setJoined(false); window.location.assign("/my/shelf"); }} /> ) : ( { const r = await mutate("waitlist.join", { itemId: data.itemId, si: data.si }); if (!r.ok) { setErr(r.error); return; } setJoined(true); window.location.reload(); }} /> )} ); } function ordinal(n: number) { const s = ["th", "st", "nd", "rd"], v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); }