"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.`}