MEGAINFRA-WEBAPP/src/lib/utils.ts
DEls e83a0d302c fix: overview counts, wrong-unit fuel limits, meter guard, en-ZA numbers, vehicle merge, docs
- Overview/Plant tiles and pages share one vehicle bucket rule (blank unit_type = fleet)
- Plant landing equipment figures were always 0; hour diffs now reject 0 baselines and >24h/day jumps
- km/L limits on plant (and L/h on vehicles) report unit-mismatch instead of false alerts
- formatPercent/formatDecimal replace display toFixed (en-ZA)
- POST /api/vehicles/[id]/merge + Merge duplicate dialog (dry run first)
- Attention links go to equipment vs vehicles by unit type; PO upload view link fixed
- Conditional hook fixed in DN document view; ESLint flat config (next lint removed in 16)
- Docs: current modules/routes/env vars; login + Ask reports plans

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:04:08 +02:00

84 lines
2.7 KiB
TypeScript

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatNumber(n: number, opts?: Intl.NumberFormatOptions) {
return new Intl.NumberFormat("en-ZA", {
maximumFractionDigits: 2,
...opts,
}).format(n);
}
export function formatCurrency(n: number, currency = "ZAR") {
return new Intl.NumberFormat("en-ZA", {
style: "currency",
currency,
maximumFractionDigits: 0,
}).format(n);
}
/** A number with exactly `digits` decimals, en-ZA separators ("0,07"). */
export function formatDecimal(n: number, digits = 1) {
return formatNumber(n, { minimumFractionDigits: digits, maximumFractionDigits: digits });
}
/**
* A percentage in en-ZA ("38,7%"). Use this, not `toFixed`, for anything
* a person reads — `toFixed` prints a decimal point beside Intl's comma.
* `signed` prefixes positive values with "+" (variances).
*/
export function formatPercent(n: number, digits = 1, opts?: { signed?: boolean }) {
const body = formatDecimal(n, digits);
const sign = opts?.signed && Number(n.toFixed(digits)) > 0 ? "+" : "";
return `${sign}${body}%`;
}
/**
* Strings that mean "OCR could not extract a real supplier" rather than
* a real vendor name. The OCR pipeline writes "Not specified in
* document" verbatim when the supplier line is missing; other placeholders
* ("Unknown", "N/A", "—") cover other pipelines. Anything in this set
* (case-insensitive, trimmed) renders as "Supplier unknown" in the UI
* instead of being displayed as if it were a real supplier.
*
* Kept as a Set literal rather than reading from config so it's easy to
* grep and extend — add more aliases here when OCR pipelines change.
*/
const UNKNOWN_SUPPLIER_ALIASES = new Set([
"",
"—",
"-",
"n/a",
"na",
"unknown",
"not specified",
"not specified in document",
]);
export function isUnknownSupplier(supplier: string | undefined | null): boolean {
if (!supplier) return true;
return UNKNOWN_SUPPLIER_ALIASES.has(supplier.trim().toLowerCase());
}
/**
* A received quantity, shown in the purchase order line's unit with the
* slip's own figure beside it.
*
* Site staff read the slip in kg; the PO is priced per ton. Showing only
* one of the two means somebody eventually reconciles the wrong number
* against the wrong document.
*/
export function formatQtyDual(
qtyReceived: number,
unit: string,
qtyRaw: number,
unitRaw: string,
): string {
const primary = unit ? `${formatNumber(qtyReceived)} ${unit}` : formatNumber(qtyReceived);
const sameUnit = !unitRaw || unitRaw.trim().toLowerCase() === unit.trim().toLowerCase();
if (sameUnit) return primary;
return `${primary} · ${formatNumber(qtyRaw)} ${unitRaw}`;
}