ThreadCount Community edition
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 38e16eb on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/* What a code scanned on the counter app's Scan tab is, and where it goes.
|
||||
*
|
||||
* One router for the one camera the tab bar puts in the middle: a shelf label opens that shelf's
|
||||
* count, a garment's barcode opens its stock line, a staff badge opens the person. Contextual scans
|
||||
* (issue, hand back, pick, count) keep their own rules and never come through here. */
|
||||
import { bcParse, itemMap, staffName, variantName, type Snapshot } from "@/lib/compute";
|
||||
|
||||
export type ScanHit =
|
||||
| { kind: "shelf"; label: string; locationId: string; href: string }
|
||||
| { kind: "garment"; label: string; itemId: string; si: number; href: string }
|
||||
| { kind: "badge"; label: string; staffId: string; href: string }
|
||||
| { kind: "inactive"; label: string; staffId: string }
|
||||
| { kind: "unknown"; code: string };
|
||||
|
||||
/** Printed on every shelf label: Code 128 of `TCL-` and the location's id. */
|
||||
export const SHELF_LABEL_PREFIX = "TCL-";
|
||||
export const shelfLabelCode = (locationId: string) => SHELF_LABEL_PREFIX + locationId;
|
||||
const SHELF_RE = /^TCL-([A-Za-z0-9]{8,40})$/;
|
||||
|
||||
/** The word the flash and the recent list show for each kind. */
|
||||
export const SCAN_KIND_LABEL: Record<"shelf" | "garment" | "badge" | "inactive", string> = {
|
||||
shelf: "Shelf label",
|
||||
garment: "Garment",
|
||||
badge: "Staff badge",
|
||||
inactive: "Staff badge",
|
||||
};
|
||||
|
||||
const digits = (x: string) => /^\d+$/.test(x);
|
||||
const noZeros = (x: string) => x.replace(/^0+(?=\d)/, "");
|
||||
|
||||
/** Does this staff number match the scanned badge? Case-insensitive; all-digit numbers also match
|
||||
* with their leading zeros stripped (a badge printing 004471 for staff number 4471). */
|
||||
export function badgeMatches(num: string, code: string): boolean {
|
||||
const a = num.trim(), b = code.trim();
|
||||
if (!a || !b) return false;
|
||||
if (a.toLowerCase() === b.toLowerCase()) return true;
|
||||
return digits(a) && digits(b) && noZeros(a) === noZeros(b);
|
||||
}
|
||||
|
||||
export function resolveScan(s: Snapshot, raw: string): ScanHit {
|
||||
const code = String(raw || "").trim();
|
||||
if (!code) return { kind: "unknown", code };
|
||||
|
||||
// 1. A shelf label.
|
||||
const m = SHELF_RE.exec(code);
|
||||
if (m) {
|
||||
// Location ids are lowercase; a code typed or read in capitals is still that shelf.
|
||||
const loc = s.locations.find((l) => (l.id === m[1] || l.id === m[1].toLowerCase()) && !l.archived);
|
||||
if (!loc) return { kind: "unknown", code };
|
||||
return { kind: "shelf", label: loc.name, locationId: loc.id, href: `/m/count/${loc.id}` };
|
||||
}
|
||||
|
||||
// 2. A garment: bound supplier codes and the generated 93XXXXXXX form, as everywhere else.
|
||||
const v = bcParse(s, code);
|
||||
if (v) {
|
||||
const it = itemMap(s)[v.itemId];
|
||||
const size = String(it?.sizes[v.si] ?? v.si);
|
||||
return { kind: "garment", label: variantName(it, size), itemId: v.itemId, si: v.si, href: `/m/line/${v.itemId}/${v.si}` };
|
||||
}
|
||||
|
||||
// 3. A staff badge carrying the staff number.
|
||||
const active = s.staff.find((st) => !st.inactive && badgeMatches(st.num, code));
|
||||
if (active) return { kind: "badge", label: staffName(active), staffId: active.id, href: `/m/person/${active.id}` };
|
||||
const gone = s.staff.find((st) => st.inactive && badgeMatches(st.num, code));
|
||||
if (gone) return { kind: "inactive", label: staffName(gone), staffId: gone.id };
|
||||
|
||||
return { kind: "unknown", code };
|
||||
}
|
||||
Reference in New Issue
Block a user