Files
threadcount-community/app/m/(app)/count/page.tsx
T
ThreadCount 1bc2de655a ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
2026-09-13 08:45:19 +10:00

79 lines
4.4 KiB
TypeScript

"use client";
/* Stocktake — choose what you're counting. One row per location that actually holds garments,
plus everything not yet placed, so nothing on the shelf is uncountable. */
import { useMemo } from "react";
import { useDerived, useSnap } from "@/lib/client";
import { UNPLACED, bcBound, daysBetween, locSubtree, locTree, onhand, touched } from "@/lib/compute";
import { INK, MBody, MEmpty, MNav, MNote, MRow, MRule, MSection, MTop } from "@/components/m";
/* "Last counted 0 days ago" and "1 days ago" are how a shelf counted this morning used to read. */
function lastCounted(last: string | undefined, today: string): string {
if (!last) return "Never counted";
const n = daysBetween(last, today);
if (n <= 0) return "Counted today";
if (n === 1) return "Counted yesterday";
return `Last counted ${n} days ago`;
}
export default function MCountStart() {
const { s } = useSnap();
const { L, variants } = useDerived();
const rows = useMemo(() => {
const lastAt: Record<string, string> = {};
for (const t of s.stocktakes) if (t.mode !== "preloved" && t.locationId && !lastAt[t.locationId]) lastAt[t.locationId] = t.date;
const out = locTree(s).map(({ loc, depth }) => {
const sub = locSubtree(s, loc.id);
// Placed on the shelf is enough to make a shelf countable. A location holding only sizes
// that have never been stocked is exactly the shelf someone needs to count in.
const mine = variants.filter((v) => sub.has(s.placed[v.key] || ""));
return { id: loc.id, name: loc.name, kind: loc.kind, depth, lines: mine.length, units: mine.reduce((t, v) => t + onhand(s, L, v.key), 0), last: lastAt[loc.id] as string | undefined };
}).filter((r) => r.lines > 0);
// Only the unplaced bucket needs a test at all — without one it would list the whole
// catalogue. A bound barcode counts as much as stock history does: somebody stood at the
// counter with the garment in hand and scanned its label onto that size, which says the size
// physically exists even when no stock figure does. The counting and variance screens filter
// the unplaced bucket with exactly this expression and all three have to agree — a room whose
// unplaced sizes are all barcode-bound and never yet stocked otherwise gets no "Not on a shelf
// yet" row here, and the one screen that could count them in is unreachable from the menu.
const loose = variants.filter((v) => !s.placed[v.key] && (touched(s, L, v.key) || !!bcBound(s, v.item, v.si)));
if (loose.length) out.push({ id: UNPLACED, name: "Not on a shelf yet", kind: "", depth: 0, lines: loose.length, units: loose.reduce((t, v) => t + onhand(s, L, v.key), 0), last: undefined });
return out;
}, [s, L, variants]);
return (
<>
<MTop title="Stocktake" right={`${rows.length} location${rows.length === 1 ? "" : "s"}`} />
<MRule />
<MBody>
<div style={{ padding: "20px 16px 22px", borderBottom: "2px solid " + INK }}>
<h2 style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 30, letterSpacing: "-0.03em", lineHeight: 1.05 }}>Where are you counting?</h2>
<p style={{ fontSize: 14, color: "var(--color-neutral-700)", marginTop: 10, lineHeight: 1.6 }}>
Scan every garment on the shelf. Each scan adds one to that line, and the expected figure stays on screen the whole way.
</p>
</div>
{rows.length === 0 ? (
<MEmpty
title="Nothing to count yet"
sub="A location shows up here once garments are placed on it. Set your shelves up in Settings on the desktop, then place each size against one."
/>
) : (
<>
<MSection label="Locations" right="Lines · units" />
{rows.map((r) => (
<MRow key={r.id} href={`/m/count/${r.id}`} mark={r.id === UNPLACED ? "mute" : "ink"}
title={<span style={{ paddingLeft: r.depth * 14 }}>{r.name}</span>}
sub={<span style={{ paddingLeft: r.depth * 14 }}>{lastCounted(r.last, s.today)}</span>}
right={<span style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 15, fontVariantNumeric: "tabular-nums" }}>{r.lines} · {r.units}</span>} />
))}
</>
)}
<MNote>A count stays open until you commit it, so you can put the phone down halfway along a shelf and pick it up again.</MNote>
</MBody>
<MNav />
</>
);
}