import type { OilItem, OilMovement } from "./data"; // From the NEUTRAL module, not from purchase-parser: this file is imported by a // client component, and the parser pulls in pdfjs and `node:module`. import { normaliseCostCode } from "./cost-code"; /** * Oil stock is the SUM of what came IN minus what went OUT. Every movement is kept, so * the first usage is a subtraction from a real history rather than a guess about what * it removed. * * `litresOnHand` is a number, and an empty drum is genuinely R0 of oil — unlike every * other "unknown" in this project, a drained drum is not missing data, it is the state. * Where nothing has EVER been received, that is different: it means nobody has entered * a delivery, so `known` is false and the page says so rather than drawing a full drum. */ export type OilStock = { item: OilItem; litres: number; /** False when the item has no movement history at all — "not entered", not "empty". */ known: boolean; /** 0–1. A drum is FULL at 1, and stock above one drum's worth simply reads full. */ fillRatio: number; /** True once a drum's worth is on hand or more. */ isFull: boolean; isEmpty: boolean; /** Whole drums held, for the "2 drums" reading next to the graphic. */ drumsHeld: number; }; /** Diesel sits in a tank, not a drum. Derived from the code so no schema change is needed. */ export function isTank(item: Pick): boolean { return (item.code || "").startsWith("FUEL"); } export function oilStock(item: OilItem, movements: OilMovement[]): OilStock { const mine = movements.filter((m) => m.item === item.id); const litres = mine.reduce( // "adjusted" carries a SIGNED litres figure (opening stock, or a manual overwrite). (sum, m) => sum + (m.kind === "used" ? -(m.litres || 0) : m.litres || 0), 0, ); const known = mine.length > 0; const capacity = item.drum_litres || 210; const fillRatio = known ? Math.max(0, Math.min(1, litres / capacity)) : 0; return { item, litres, known, fillRatio, isFull: known && litres >= capacity, isEmpty: known && litres <= 0, drumsHeld: known ? Math.max(0, Math.floor(litres / capacity)) : 0, }; } /** Does this purchase-order cost code name an oil store rather than a machine? */ export function storeCodeOf(costCode: string, items: OilItem[]): OilItem | null { const wanted = normaliseCostCode(costCode); if (!wanted) return null; return items.find((i) => normaliseCostCode(i.store_cost_code || "") === wanted) ?? null; } /** What the drum's QR encodes: the oil name and its item code. */ export function oilQrPayload(item: Pick): string { return `${(item.name || "").trim()} | ${(item.code || "").trim()}`; }