"use client"; /* Person record — who they are, what they're holding, what has happened, and the three things you can do about it. Issuing starts here: 1B, person first. */ import Link from "next/link"; import { useMemo, useState } from "react"; import { useParams } from "next/navigation"; import { useSnap } from "@/lib/client"; import { fmtDate, itemMap, staffName, variantName } from "@/lib/compute"; import { GROUND, INK, MBar, MBody, MEmpty, MError, MRow, MRule, MSection, MTop } from "@/components/m"; import { MPersonHead, useHeld } from "@/components/MPerson"; export default function MPersonPage() { const { s, isAdmin, mutate } = useSnap(); const id = String(useParams().id || ""); const st = s.staff.find((x) => x.id === id); const held = useHeld(s, id); const byId = useMemo(() => itemMap(s), [s]); // Shown once, then gone: the code is a credential and is never in the snapshot. const [code, setCode] = useState(null); const [busy, setBusy] = useState(false); const [err, setErr] = useState(""); const origin = typeof window === "undefined" ? "threadcount.tech" : window.location.host; const history = useMemo(() => { if (!st) return []; const out: { text: string; date: string }[] = []; for (const i of s.issues) { if (i.staffId !== id) continue; const it = byId[i.itemId]; const size = String(it?.sizes[i.si] ?? i.si); out.push({ text: `Issued ${i.qty} × ${variantName(it, size)}`, date: i.date }); if (i.returned) out.push({ text: `${i.returned.cond} — ${variantName(it, size)}`, date: i.returned.date }); if (i.handedIn) out.push({ text: `Handed in — ${variantName(it, size)}`, date: i.handedIn }); } return out.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)).slice(0, 25); }, [s, id, st, byId]); if (!st) return (<>); const total = held.reduce((t, h) => t + h.qty, 0); /* Issue, Exchange and Return are docked at the foot of the window with nothing underneath them, so Android draws the gesture handle across their bottom edge. The inset goes inside the bar the way the shared MBar and MAction take it — same custom property, so an ancestor that zeroes it for a bar sitting mid-screen would zero this one too — and the accent still runs to the bottom of the glass while the words stay above the handle. Without it the lower third of "Issue" is untappable, and this is the row a counter hand hits all day. */ const SAFE_BOTTOM = "var(--tcx-safe-bottom, env(safe-area-inset-bottom, 0px))"; const foot: React.CSSProperties = { flex: 1, minHeight: `calc(64px + ${SAFE_BOTTOM})`, display: "flex", alignItems: "center", padding: `0 16px ${SAFE_BOTTOM}`, fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 13, letterSpacing: "0.08em", textTransform: "uppercase", textDecoration: "none" }; return ( <> {held.length === 0 ?
Nothing out at the moment.
: held.map((h) => {h.qty}} />)} {code ? ( <>
{code}

Read this out or write it down now — it can't be shown again. They go to{" "} {origin}/my, choose “I have a code”, and set an email and password.

setCode(null)} /> ) : st.selfEmail ? (
Signed up as {st.selfEmail} — they can look up their own record instead of coming to the counter.
) : ( <>

{st.selfCode ? "A code is out but hasn’t been used. Make a new one if they’ve lost it — the old one stops working." : "Give them a code and they can check what they hold on their own phone. Read-only."}

setErr("")} /> {isAdmin && ( { setBusy(true); setErr(""); const r = await mutate<{ code: string }>("staff.selfCode", { id: st.id }); setBusy(false); if (!r.ok) { setErr(r.error); return; } setCode(r.result.code); }} /> )} )} {history.length === 0 ?
Nothing recorded for {staffName(st)} yet.
: history.map((h, i) => (
{h.text} {fmtDate(h.date)}
))}
Issue Exchange Return
); }