// lib/format.ts // Defensive formatters. All PocketBase text fields may come back as // null, undefined, or — for some columns historically stored as // numerics — actual numbers. String() coerce first. const PLACEHOLDER = "—"; export function text(value: unknown, fallback: string = PLACEHOLDER): string { if (value === null || value === undefined) return fallback; const s = String(value).trim(); return s.length === 0 ? fallback : s; } export function number(value: unknown, fallback: number = 0): number { if (value === null || value === undefined || value === "") return fallback; const n = Number(value); return Number.isFinite(n) ? n : fallback; } export function formatDate(iso: string | null | undefined): string { if (!iso) return PLACEHOLDER; const d = new Date(iso); if (Number.isNaN(d.getTime())) return PLACEHOLDER; // SA-style dd MMM yyyy (e.g. "08 Jul 2026"). Uses user locale-stable // English month names to avoid I18n drift in screenshots. const day = String(d.getDate()).padStart(2, "0"); const month = d.toLocaleString("en-US", { month: "short", timeZone: "UTC" }); const year = d.getUTCFullYear(); return `${day} ${month} ${year}`; } /** Normalise a PocketBase `date` field value for use as the `value` of * an ``. PB returns these as full ISO timestamps like * `"2018-08-31 00:00:00.000Z"`; HTML5 date inputs only accept the bare * `YYYY-MM-DD` prefix — anything longer is rejected and the field renders * empty, which made saved dates look like they hadn't persisted when * the user re-opened the edit modal. * * Returns "" for null/empty/non-date inputs so the input stays blank * instead of showing the raw ISO string. */ export function dateInputValue(value: string | null | undefined): string { if (typeof value !== "string") return ""; const s = value.trim(); if (!s) return ""; // Anything starting with YYYY-MM-DD is a date — keep just the prefix. if (/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0, 10); return ""; } export function formatDateTime(iso: string | null | undefined): string { if (!iso) return PLACEHOLDER; const d = new Date(iso); if (Number.isNaN(d.getTime())) return PLACEHOLDER; const day = String(d.getDate()).padStart(2, "0"); const month = d.toLocaleString("en-US", { month: "short", timeZone: "UTC" }); const year = d.getUTCFullYear(); const hh = String(d.getUTCHours()).padStart(2, "0"); const mm = String(d.getUTCMinutes()).padStart(2, "0"); return `${day} ${month} ${year} ${hh}:${mm}`; } /** Map raw `enterprise_status` text into the three buckets we render. */ export function deriveCompanyStatus( enterpriseStatus: string | null, isActive: boolean, ): "active" | "inactive" | "in_process" | "deregistered" { const s = (enterpriseStatus ?? "").toLowerCase(); if (s.includes("deregist")) return "deregistered"; if (s.includes("in process") || s.includes("process")) return "in_process"; if (s.includes("in business") || s.includes("active")) return "active"; if (s.includes("inactive")) return "inactive"; // Fall back to the boolean if text status is ambiguous. return isActive ? "active" : "inactive"; } /** Build a PocketBase filter string safely. Escapes single quotes * by doubling them (PB's own escape convention). */ export function quoteFilter(input: string): string { return input.replace(/'/g, "''"); } /** Loose email format check. Returns true if the string looks like * a real address — not a typo, not empty, not missing the domain. */ export function isValidEmail(value: unknown): boolean { if (typeof value !== "string") return false; const v = value.trim(); if (!v) return false; // Standard-ish: local@domain.tld, no spaces, must contain @ and a dot in the domain // Allow + (plus addressing), dots in local part, etc. return /^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/.test(v); } /** True if the recipient is missing OR has an invalid format. */ export function isMissingOrInvalidEmail(value: unknown): boolean { return !isValidEmail(value); }