Files
threadcount-community/components/orders/toOrder.ts
T
ThreadCount c89010a4f1 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 f976bd5 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
2026-09-15 23:18:05 +10:00

78 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* The To order list, pure. Membership comes only from atReorderVariants() (lib/portalcounts.ts) so
* the Orders badge, the Stock filter and this list agree; this file adds the per-line fields. */
import { atReorderVariants } from "@/lib/portalcounts";
import {
forecastFor, key, label, onOrderMap, onhand, reorderAt, supplierCodeOf, supplierInfo,
type Item, type Ledger, type OrderRec, type Snapshot,
} from "@/lib/compute";
export type ToOrderLine = {
key: string; itemId: string; si: number; size: string; name: string;
supplier: string; code: string; oh: number; ro: number; onOrder: number;
qty: number; perWeek: number | null; runsOut: boolean; cost: number;
};
export type SupplierGroup = {
supplier: string; lead: number | null; email: string;
lines: ToOrderLine[]; drafts: OrderRec[];
};
export const supplierOf = (s: Snapshot, it: Item | undefined) => it?.supplier || s.settings.suppliers[0] || "Supplier";
export function supplierMeta(s: Snapshot, name: string): { lead: number | null; email: string } {
const info = supplierInfo(s, name);
return { lead: info?.lead && info.lead > 0 ? info.lead : null, email: info?.email || "" };
}
/** Stock on every open order, drafts included. Drafts are raised from the same panel as the
* suggested lines, so a quantity already sitting in any draft (a replenishment draft too) must not
* be suggested a second time. */
export function onOrderByKey(s: Snapshot, byId: Record<string, Item>): Record<string, number> {
return onOrderMap(s, byId).byKey;
}
/** One size's figures, whether it came from the reorder rule or was added by hand. */
export function lineFor(s: Snapshot, L: Ledger, byId: Record<string, Item>, oo: Record<string, number>, itemId: string, si: number): ToOrderLine | null {
const it = byId[itemId];
if (!it || si < 0 || si >= it.sizes.length) return null;
const k = key(itemId, si);
const oh = onhand(s, L, k), ro = reorderAt(s, k);
const onOrder = oo[k] || 0;
const f = forecastFor(s, L, byId, k);
return {
key: k, itemId, si, size: String(it.sizes[si]), name: label(it), supplier: supplierOf(s, it),
code: supplierCodeOf(s, k), oh, ro, onOrder,
qty: Math.max(ro * 2 - oh - onOrder, 0),
perWeek: f.avgWeekly === null ? null : Math.round(f.avgWeekly * 10) / 10,
runsOut: f.runsOutBeforeDelivery || (oh <= 0 && f.avgWeekly !== null),
cost: it.cost || 0,
};
}
export const sortLines = (a: ToOrderLine, b: ToOrderLine) =>
Number(b.runsOut) - Number(a.runsOut) || a.name.localeCompare(b.name) || a.si - b.si;
/** Supplier panels AZ: every supplier with a size at reorder or a draft with lines. */
export function toOrderGroups(s: Snapshot, L: Ledger, byId: Record<string, Item>): SupplierGroup[] {
const oo = onOrderByKey(s, byId);
const by = new Map<string, SupplierGroup>();
const group = (name: string) => {
let g = by.get(name);
if (!g) { g = { supplier: name, ...supplierMeta(s, name), lines: [], drafts: [] }; by.set(name, g); }
return g;
};
for (const v of atReorderVariants(s, L)) {
const line = lineFor(s, L, byId, oo, v.itemId, v.si);
if (line) group(line.supplier).lines.push(line);
}
for (const o of s.orders) if (o.status === "Draft" && o.lines.length > 0) group(o.supplier || "No supplier").drafts.push(o);
for (const g of by.values()) { g.lines.sort(sortLines); g.drafts.sort((a, b) => a.code.localeCompare(b.code)); }
return [...by.values()].sort((a, b) => a.supplier.localeCompare(b.supplier));
}
/** A draft's value at catalogue cost (a draft has no receipts). */
export const draftValue = (o: OrderRec, byId: Record<string, Item>) => o.lines.reduce((t, l) => t + l.qty * (byId[l.itemId]?.cost || 0), 0);
/** The size index of an order line, or -1. */
export const siOf = (it: Item | undefined, size: string) => (it ? it.sizes.map(String).indexOf(size) : -1);