mega-plant/lib/mechanics.ts

30 lines
1.4 KiB
TypeScript
Raw Normal View History

Add Running Costs sub-pages: insurance, financing, purchases, oil, operators, mechanics Splits the Running Costs group into six purpose-built pages, each with its own collection and its own source data, plus a coordinate parser for purchase orders. Insurance (R503 330/yr, 20 vehicles) A premium can move every month or three, so it is stored as a RATE PERIOD in insurance_rates and each month's cost is resolved against the rate in force that month. cost_lines insurance rows are derived from those periods. An absent rate is null, never 0 — nobody has entered the date, which is not the same as being insured for free. Financing (20 agreements, R428 585/mo) Instalment from the sheet; start date, paid-off date and the settled toggle are human-entered and preserved across re-seeds. Months-left and outstanding are null until a date is entered, never R0 on a live debt. The two paid-off agreements (WB02, DT01) show as settled and are excluded from the run-rate. Purchases + Oil Stock Uploads PO PDFs the way /income/invoices does. The parser reads columns by x-position, because plain text extraction reorders them, and refuses any PO whose lines do not sum to the sub-total. A cost code either names a machine ("SS-02" -> SS02) or a stock store ("MPLANT, ENGOIL") — never both, never guessed. Oil POs move litres into oil_stock_movements instead of onto a machine; stock is the sum of movements so the first usage is a subtraction from history. Operators (19) and Mechanics (2) Both are people with an employee number, separately reconcilable by the seed so someone added in the app survives. Mechanics get a badge QR; a mechanic with no employee number cannot be printed, because the code is what the QR encodes. Cross-cutting - The seed no longer deletes operators, mechanics or oil items: people are added in the app, and those rows exist in no file. - Re-pointing allocations that PocketBase clears because `assets` is rebuilt with new ids each seed (operators, purchase lines). - One QR encoder for screen and PDF, so a printed code cannot drift from the one on screen. pdf-lib writes real vector PDFs; pdfjs-dist only reads them. - Every figure is null-safe: unknown is never rendered as zero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-08 23:06:27 +00:00
import type { Mechanic } from "./data";
/**
* What a mechanic's badge QR actually encodes: their NAME and EMPLOYEE NUMBER
* together (client ruling, 2026-10-08).
*
* Deliberately NOT a URL. This app sits behind a login and is only reachable inside
* the network, so a badge scanned on the yard would dead-end at a sign-in screen — and
* a badge is worthless if scanning it tells you nothing you could not have written on
* the card. Plain text reads the same on any scanner, survives the app moving host,
* and stays short enough that a cheap camera picks it up at an angle.
*
* ` | ` separates the two halves so the decoded string is both human-readable and
* trivially split back into fields.
*/
export function mechanicQrPayload(m: Pick<Mechanic, "name" | "employee_code">): string {
return `${(m.name || "").trim()} | ${(m.employee_code || "").trim()}`;
}
/** Split a decoded payload back into its two halves. Unknown input is not guessed at. */
export function parseMechanicQr(payload: string): { name: string; employeeCode: string } | null {
const parts = payload.split("|").map((p) => p.trim());
if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
return { name: parts[0], employeeCode: parts[1] };
}
/** A person with no employee number cannot be issued a badge — there is nothing to encode. */
export function isBadgeReady(m: Pick<Mechanic, "employee_code">): boolean {
return Boolean((m.employee_code || "").trim());
}