Moves normHeader/resolveColumn/parseDate/toNumber out of fuel-import-parse into a new src/lib/import-parse.ts so plant-hours can reuse them. Adds parseTime (HH:MM, 24h, single-digit hour normalised). Adds the PLANT_COL_ALIASES table for the plant-hours route. fuel-import-parse becomes a thin re-export shim — the fuel-logs route and its existing test file are unchanged.
124 lines
No EOL
4.8 KiB
TypeScript
124 lines
No EOL
4.8 KiB
TypeScript
/**
|
|
* Shared parsing helpers for the import routes (fuel-logs, plant-hours).
|
|
* Extracted from src/lib/fuel-import-parse.ts so that plant-hours can
|
|
* reuse the column-resolution + date-parsing logic and add a new
|
|
* parseTime helper without duplicating code.
|
|
*
|
|
* All four helpers are stateless and side-effect free. fuel-import-parse
|
|
* re-exports the three legacy helpers so existing consumers keep working.
|
|
*/
|
|
|
|
/** Header aliases for the plant-hours import (13 logical columns). */
|
|
export const PLANT_COL_ALIASES: Record<string, string[]> = {
|
|
date_logged: ["date logged", "date_logged", "date"],
|
|
vehicle_qr: ["vehicle qr", "vehicle_qr", "qr", "qr_code_id"],
|
|
vehicle_desc: ["vehicle", "vehicle description", "vehicle_description", "description"],
|
|
start_date: ["start date", "start_date"],
|
|
end_date: ["end date", "end_date"],
|
|
start_time: ["start time", "start_time"],
|
|
end_time: ["end time", "end_time"],
|
|
start_hours: ["start hours", "start_hours"],
|
|
end_hours: ["end hours", "end_hours"],
|
|
hours: ["hours"],
|
|
notes: ["notes"],
|
|
hour_meter: ["hour meter", "hour_meter", "hour_meter_budget", "budget"],
|
|
cost_per_hour: ["cost / hr", "cost/hr", "cost_per_hour", "cost"],
|
|
};
|
|
|
|
/** Header aliases for the fuel-logs import (legacy, kept for the fuel route). */
|
|
export const FUEL_COL_ALIASES: Record<string, string[]> = {
|
|
date: ["date logged", "date_logged", "date"],
|
|
vehicle_qr: ["vehicle qr", "vehicle_qr", "qr", "qr code id", "qr_code_id"],
|
|
vehicle_desc: ["vehicle", "vehicle description", "vehicle_description", "description"],
|
|
odo: ["km's/ hours", "kms/ hours", "km/hours", "odo_reading", "odometer", "hour_meter", "hour meter"],
|
|
litres: ["litres pumped", "litres", "liters"],
|
|
site: ["current site", "site", "current_site"],
|
|
consumption: ["latest consumption", "latest_consumption", "consumption"],
|
|
};
|
|
|
|
/** Normalise a header for fuzzy matching. */
|
|
export function normHeader(s: unknown): string {
|
|
return typeof s === "string"
|
|
? s.replace(/\s+/g, " ").trim().toLowerCase()
|
|
: "";
|
|
}
|
|
|
|
/**
|
|
* Resolve the original header text for a logical column key.
|
|
* Returns the EXACT header string the spreadsheet used (so the row
|
|
* lookup uses the same key the source file provided), or null if no
|
|
* alias matched.
|
|
*/
|
|
export function resolveColumn(
|
|
headers: string[],
|
|
key: keyof typeof PLANT_COL_ALIASES | keyof typeof FUEL_COL_ALIASES,
|
|
aliasMap: Record<string, string[]> = PLANT_COL_ALIASES as Record<string, string[]>,
|
|
): string | null {
|
|
const target = aliasMap[key].map(normHeader);
|
|
for (const h of headers) {
|
|
if (target.includes(normHeader(h))) return h;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Parse the date column into an ISO `YYYY-MM-DD` string.
|
|
* Primary: `DD/MM/YYYY`. Falls back to `YYYY-MM-DD` and Excel serials.
|
|
*/
|
|
export function parseDate(input: unknown): string | null {
|
|
if (input === null || input === undefined) return null;
|
|
if (typeof input === "number" && Number.isFinite(input) && input > 25569) {
|
|
const d = new Date(Math.round((input - 25569) * 86400 * 1000));
|
|
if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
|
|
}
|
|
const s = String(input).trim();
|
|
if (!s) return null;
|
|
let m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s);
|
|
if (m) {
|
|
const dd = Number(m[1]);
|
|
const mm = Number(m[2]);
|
|
const yyyy = Number(m[3]);
|
|
if (mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31) {
|
|
const iso = new Date(Date.UTC(yyyy, mm - 1, dd));
|
|
if (!Number.isNaN(iso.getTime())) return iso.toISOString().slice(0, 10);
|
|
}
|
|
return null;
|
|
}
|
|
m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s);
|
|
if (m) {
|
|
const iso = new Date(
|
|
Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])),
|
|
);
|
|
if (!Number.isNaN(iso.getTime())) return iso.toISOString().slice(0, 10);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Coerce a cell value to a finite number, or null if empty / non-numeric. */
|
|
export function toNumber(input: unknown): number | null {
|
|
if (input === null || input === undefined) return null;
|
|
if (typeof input === "number") return Number.isFinite(input) ? input : null;
|
|
if (typeof input === "string") {
|
|
const trimmed = input.trim();
|
|
if (!trimmed) return null;
|
|
const n = Number(trimmed);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Parse a time cell into `HH:MM` (24h). Returns null on malformed input.
|
|
* Single-digit hours are normalised to two digits (`6:00` → `06:00`).
|
|
*/
|
|
export function parseTime(input: unknown): string | null {
|
|
if (input === null || input === undefined) return null;
|
|
const s = String(input).trim();
|
|
if (!s) return null;
|
|
const m = /^(\d{1,2}):(\d{2})$/.exec(s);
|
|
if (!m) return null;
|
|
const hh = Number(m[1]);
|
|
const mm = Number(m[2]);
|
|
if (hh < 0 || hh > 23 || mm < 0 || mm > 59) return null;
|
|
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
|
|
} |