0910bc32c1
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).
69 lines
3.2 KiB
TypeScript
69 lines
3.2 KiB
TypeScript
/* 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 };
|
|
}
|