/** * 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 = { 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 = { 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 = PLANT_COL_ALIASES as Record, ): 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")}`; }