mega-plant/lib/oil.ts
David df53130861 Add Workforce pages, Plant Clock tab bar and oil & diesel book-out
- 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>
2026-10-22 11:39:18 +02:00

67 lines
No EOL
2.7 KiB
TypeScript
Raw Permalink 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.

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()}`;
}