- 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>
84 lines
2.7 KiB
TypeScript
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}`;
|
|
}
|