- Workforce: Employees register, per-employee dashboards, Attendance table - Plant Clock: scan-only clock, bottom tab bar (Employees / Oil & Diesel / Job cards) - Oil & Diesel stock: diesel tank (650 L, opening 589 L), level overwrite dialog, QR on each card, book-out screen with searchable vehicle picker, filterable "Booked out" table (today/week/month/all/range) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
67 lines
No EOL
2.7 KiB
TypeScript
67 lines
No EOL
2.7 KiB
TypeScript
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<OilItem, "code">): 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<OilItem, "name" | "code">): string {
|
||
return `${(item.name || "").trim()} | ${(item.code || "").trim()}`;
|
||
} |