c89010a4f1
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from f976bd5 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
75 lines
3.4 KiB
TypeScript
75 lines
3.4 KiB
TypeScript
"use client";
|
||
import Link from "next/link";
|
||
import { useMemo, useState } from "react";
|
||
import { useDerived, useSnap } from "@/lib/client";
|
||
import { QueueGroup, QueueRow } from "@/components/portal";
|
||
import { ErrorLine } from "@/components/ui";
|
||
import { NewOrderDialog } from "@/components/dialogs";
|
||
import { useServerCounts } from "@/lib/portalcounts";
|
||
import { bagStock, isPickable, useRequestActions, useRequests, type RequestRow } from "@/components/requests/RequestList";
|
||
import { relativeDay } from "@/lib/today";
|
||
import { Lines } from "./Lines";
|
||
|
||
export default function PickGroup() {
|
||
const { s } = useSnap();
|
||
const { L, byId } = useDerived();
|
||
const server = useServerCounts();
|
||
const { data, error, loading, reload } = useRequests();
|
||
const { act, error: actErr } = useRequestActions(reload);
|
||
const [orderFor, setOrderFor] = useState<{ r: RequestRow; short: RequestRow["bag"] } | null>(null);
|
||
|
||
const rows = useMemo(() => {
|
||
if (!data) return [];
|
||
return data.requests
|
||
.filter(isPickable)
|
||
.sort((a, b) => ((a.decidedAt || a.createdAt) < (b.decidedAt || b.createdAt) ? -1 : 1))
|
||
.map((r) => ({ r, stock: bagStock(s, L, r) }));
|
||
}, [data, s, L]);
|
||
|
||
const count = data ? rows.length : server.pick;
|
||
|
||
return (
|
||
<>
|
||
<QueueGroup id="pick" icon="bag" title="Pick for approved requests" count={count}>
|
||
{actErr && <div style={{ padding: "0 16px 10px" }}><ErrorLine msg={actErr} /></div>}
|
||
{!data && error && !loading && (
|
||
<div className="tc-qrow">
|
||
<div className="tc-qrow-main tc-qrow-meta">Requests couldn’t be loaded.</div>
|
||
<div className="tc-qrow-actions"><button type="button" className="btn btn-ghost" onClick={() => reload()}>Try again</button></div>
|
||
</div>
|
||
)}
|
||
{rows.map(({ r, stock }) => {
|
||
const lines = (r.bag.length ? r.bag : r.lines).map((l) => ({ garment: l.item, size: l.size, qty: l.qty }));
|
||
const when = relativeDay(r.decidedAt, s);
|
||
return (
|
||
<QueueRow
|
||
key={r.id}
|
||
age={r.code}
|
||
ageLabel={r.ward.slice(0, 10)}
|
||
title={<Link href={`/app/requests?open=${encodeURIComponent(r.id)}`} style={{ color: "inherit" }}>{r.staffName}</Link>}
|
||
meta={<>
|
||
<Lines lines={lines} />
|
||
{r.managerName ? ` · approved by ${r.managerName}` : " · approved"}{when ? ` ${when}` : ""}
|
||
{" · "}
|
||
{stock.inStock ? "in stock" : <span style={{ color: "var(--color-accent-700)", fontWeight: 600 }}>none on the shelf</span>}
|
||
</>}
|
||
actions={stock.inStock
|
||
? <button type="button" className="btn btn-secondary" aria-label={`Start picking ${r.code}`} onClick={() => act("request.pick", { id: r.id })}>Start picking</button>
|
||
: <button type="button" className="btn btn-ghost" aria-label={`Order in the garments for ${r.code}`} onClick={() => setOrderFor({ r, short: stock.shortLines })}>Order it in</button>}
|
||
/>
|
||
);
|
||
})}
|
||
</QueueGroup>
|
||
{orderFor && (
|
||
<NewOrderDialog
|
||
onClose={() => setOrderFor(null)}
|
||
initOrderFor="Staff Member"
|
||
initStaffId={orderFor.r.staffId}
|
||
initLines={orderFor.short.map((l) => ({ itemId: l.itemId, size: l.size, qty: l.qty }))}
|
||
initSupplier={orderFor.short[0] ? byId[orderFor.short[0].itemId]?.supplier : undefined}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
}
|