"use client"; /* Person › History: what was issued, handed back and handed in, grouped per day, newest first; and * their staff app account, with the admin-only code. */ import { useEffect, useMemo, useState } from "react"; import { useSnap } from "@/lib/client"; import { fmtDate, itemMap, label, type IssueRec } from "@/lib/compute"; import { MButton, MEmpty, MONO, MPill, MRow, MSection } from "@/components/m"; import type { PersonTabProps } from "@/components/m/handback/HandBackTab"; type Group = { key: string; date: string; kind: number; verb: string; parts: Record; signed: boolean; at: string }; const COND_NOTE: Record = { "Returned - Damaged": " (damaged)", "Written Off": " (condemned)", Lost: " (lost)" }; /** "Today", "12 Aug", or "12 Aug 2025" for another year (the mockup's short date). */ function dayLabel(iso: string, today: string): string { if (!iso || iso.length < 10) return fmtDate(iso); if (iso.slice(0, 10) === today) return "Today"; const d = new Date(+iso.slice(0, 4), +iso.slice(5, 7) - 1, +iso.slice(8, 10)); if (Number.isNaN(d.getTime())) return fmtDate(iso); const mon = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][d.getMonth()]; const short = `${d.getDate()} ${mon}`; return iso.slice(0, 4) === (today || "").slice(0, 4) ? short : `${short} ${iso.slice(0, 4)}`; } const lower = (t: string) => t; // catalogue names keep their own casing (acronyms such as RN) export default function HistoryTab({ staffId, setBar, onError }: PersonTabProps) { const { s, isAdmin, mutate } = useSnap(); const st = s.staff.find((x) => x.id === staffId); // 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); useEffect(() => { setBar(null); }, [setBar]); const groups = useMemo(() => { const byId = itemMap(s); const m: Record = {}; const put = (date: string, kind: number, verb: string, i: IssueRec, note = "") => { const gk = `${date}|${kind}`; const g = (m[gk] ||= { key: gk, date, kind, verb, parts: {}, signed: false, at: "" }); const it = byId[i.itemId]; const pk = `${i.itemId}:${i.si}${note}`; const p = (g.parts[pk] ||= { name: `${lower(label(it))} ${String(it?.sizes[i.si] ?? i.si)}${note}`, qty: 0 }); p.qty += i.qty; if (kind === 0) { if (i.receipt) g.signed = true; if ((i.createdAt || "") > g.at) g.at = i.createdAt || ""; } }; for (const i of s.issues) { if (i.staffId !== staffId) continue; put(i.date, 0, "Issued", i); if (i.returned) put(i.returned.date, 1, "Handed back", i, COND_NOTE[i.returned.cond] || ""); if (i.handedIn) put(i.handedIn, 2, "Handed in", i); } return Object.values(m) .sort((a, b) => b.date.localeCompare(a.date) || a.kind - b.kind) .slice(0, 25) .map((g) => ({ key: g.key, date: g.date, signed: g.signed, title: `${g.verb} ${Object.values(g.parts).map((p) => { // A condition note sits after the count: "fleece L ×1 (damaged)". const at = p.name.indexOf(" ("); return at > 0 ? `${p.name.slice(0, at)} ×${p.qty}${p.name.slice(at)}` : `${p.name} ×${p.qty}`; }).join(", ")}`, })); }, [s, staffId]); if (!st) return null; const account = st.selfEmail ? `Signed up · ${st.selfEmail}` : st.selfCode ? "Code out, not used" : "No account"; const generate = async () => { setBusy(true); const r = await mutate<{ code: string }>("staff.selfCode", { id: st.id }); setBusy(false); if (!r.ok) { onError(r.error); return; } setCode(r.result.code); }; return ( <> {groups.length === 0 ? : groups.map((g) => Signed : undefined} />)} {code ? ( <>
{code}
setCode(null)} /> ) : isAdmin && !st.selfEmail ? ( ) : null} ); }