96d5c10537
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 49da3a4 on 2026-09-13. Licensed under the Functional Source License (FSL-1.1-ALv2).
76 lines
4.2 KiB
TypeScript
76 lines
4.2 KiB
TypeScript
"use client";
|
||
/* The call list — longest wait first, because that’s the one somebody is annoyed about. */
|
||
import { useMemo, useState } from "react";
|
||
import { useSnap } from "@/lib/client";
|
||
import { daysBetween, itemMap, label, staffMap, staffName } from "@/lib/compute";
|
||
import { INK, MBody, MEmpty, MError, MNav, MRow, MRule, MSection, MTop } from "@/components/m";
|
||
|
||
export default function MPickups() {
|
||
const { s, mutate, busy } = useSnap();
|
||
const [err, setErr] = useState("");
|
||
|
||
const rows = useMemo(() => {
|
||
const staffById = staffMap(s);
|
||
const byId = itemMap(s);
|
||
return s.pickups
|
||
.filter((p) => !p.pickedUp)
|
||
.map((p) => ({
|
||
...p,
|
||
who: staffById[p.staffId],
|
||
phone: staffById[p.staffId]?.phone || "",
|
||
days: daysBetween(p.received, s.today),
|
||
what: p.lines.map((l) => `${label(byId[l.itemId])} · ${l.size}${l.qty > 1 ? ` ×${l.qty}` : ""}`).join(", "),
|
||
}))
|
||
.sort((a, b) => b.days - a.days);
|
||
}, [s]);
|
||
|
||
const act = async (op: string, id: string) => {
|
||
const r = await mutate(op, { id });
|
||
if (!r.ok) setErr(r.error);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<MTop title="Pickups" back right={rows.length ? `${rows.length} waiting` : undefined} />
|
||
<MRule />
|
||
<MError msg={err} onDismiss={() => setErr("")} />
|
||
<MBody>
|
||
{rows.length === 0 ? (
|
||
<MEmpty title="Nobody is waiting" sub="Everything that has come in has been collected." />
|
||
) : (
|
||
<>
|
||
<MSection label="Waiting" right="Longest first" />
|
||
{rows.map((p) => (
|
||
<div key={p.id} style={{ padding: 16, background: p.days > 10 ? "#fff" : "var(--color-bg)", borderBottom: "1px solid var(--color-divider)", display: "flex", gap: 12 }}>
|
||
<span aria-hidden="true" style={{ width: 4, flex: "0 0 4px", background: p.days > 10 ? "var(--color-accent)" : INK, alignSelf: "stretch" }} />
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
||
<span style={{ flex: 1, fontSize: 16.5, fontWeight: 700 }}>{staffName(p.who, "Staff member")}</span>
|
||
<span style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 15, color: p.days > 10 ? "var(--color-accent-700)" : "var(--color-neutral-600)", fontVariantNumeric: "tabular-nums" }}>{p.days}d</span>
|
||
</div>
|
||
<div style={{ fontSize: 13.5, color: "var(--color-neutral-600)", marginTop: 3 }}>{p.what} · {p.orderCode}</div>
|
||
<div style={{ display: "flex", gap: 8, marginTop: 12, flexWrap: "wrap" }}>
|
||
{p.phone && (
|
||
<a href={`tel:${p.phone.replace(/\s+/g, "")}`}
|
||
style={{ minHeight: 44, display: "inline-flex", alignItems: "center", padding: "0 14px", border: "2px solid " + INK, color: INK, textDecoration: "none", fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 13, letterSpacing: "0.08em", textTransform: "uppercase" }}>Call</a>
|
||
)}
|
||
<button onClick={() => act("pickup.contacted", p.id)} disabled={busy || p.contacted}
|
||
style={{ minHeight: 44, padding: "0 14px", border: "2px solid " + INK, background: p.contacted ? INK : "transparent", color: p.contacted ? "var(--color-bg)" : INK, fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 13, letterSpacing: "0.08em", textTransform: "uppercase", cursor: p.contacted ? "default" : "pointer", opacity: busy ? 0.5 : 1 }}>
|
||
{p.contacted ? "Contacted" : "Mark contacted"}
|
||
</button>
|
||
<button onClick={() => act("pickup.pickedUp", p.id)} disabled={busy}
|
||
style={{ minHeight: 44, padding: "0 14px", border: "2px solid var(--color-accent)", background: "var(--color-accent)", color: "#fff", fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 13, letterSpacing: "0.08em", textTransform: "uppercase", cursor: "pointer", opacity: busy ? 0.5 : 1 }}>
|
||
Collected
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</>
|
||
)}
|
||
</MBody>
|
||
<MNav />
|
||
</>
|
||
);
|
||
}
|