/* ── Pure helpers extracted from QuoteGenerator.tsx ── */ /** Recursively strip null values from service objects (Zod v4 optional() rejects null). */ export function sanitizeServices(svcs: any[]): any[] { return svcs.map((s) => { const clean: Record = {}; for (const [k, v] of Object.entries(s)) { if (v === null) continue; clean[k] = v; } return clean; }); } export function normalizeCustomerInfo(raw: any) { if (!raw || typeof raw !== 'object') return null; const parsedNestedCustomerInfo = (() => { const nested = raw.customerInfo; if (!nested) return null; if (typeof nested === 'string') { try { return JSON.parse(nested); } catch { return null; } } return typeof nested === 'object' ? nested : null; })(); const pick = (...values: any[]) => { for (const value of values) { if (typeof value === 'string' && value.trim()) return value; if (typeof value === 'number' && Number.isFinite(value)) return String(value); } return ''; }; return { name: pick(raw.name, parsedNestedCustomerInfo?.name, raw.customerName, parsedNestedCustomerInfo?.customerName, raw.customer, parsedNestedCustomerInfo?.customer), phone: pick(raw.phone, parsedNestedCustomerInfo?.phone, raw.customerPhone, parsedNestedCustomerInfo?.customerPhone, raw.customer_phone, parsedNestedCustomerInfo?.customer_phone), vehicleInfo: pick(raw.vehicleInfo, parsedNestedCustomerInfo?.vehicleInfo, raw.vehicle, parsedNestedCustomerInfo?.vehicle, raw.vehicle_info, parsedNestedCustomerInfo?.vehicle_info), vin: pick(raw.vin, parsedNestedCustomerInfo?.vin, raw.VIN, parsedNestedCustomerInfo?.VIN), mileage: pick(raw.mileage, parsedNestedCustomerInfo?.mileage, raw.miles, parsedNestedCustomerInfo?.miles), roNumber: pick(raw.roNumber, parsedNestedCustomerInfo?.roNumber, raw.repairOrderNumber, parsedNestedCustomerInfo?.repairOrderNumber, raw.RO, parsedNestedCustomerInfo?.RO, raw.ro, parsedNestedCustomerInfo?.ro, raw.ro_number, parsedNestedCustomerInfo?.ro_number), repairOrderId: pick(raw.repairOrderId, parsedNestedCustomerInfo?.repairOrderId), serviceAdvisor: pick(raw.serviceAdvisor, parsedNestedCustomerInfo?.serviceAdvisor, raw.advisorName, parsedNestedCustomerInfo?.advisorName, raw.serviceAdvisorName, parsedNestedCustomerInfo?.serviceAdvisorName, raw.advisor, parsedNestedCustomerInfo?.advisor), }; } export function formatQuoteCurrency(amount: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); } export function formatQuoteDate(iso: string | undefined | null): string { if (!iso) return '—'; try { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(new Date(iso)); } catch { return '—'; } } export function recentQuoteDate(quote: { createdAt?: string; lastUpdated?: string }): string { return quote.createdAt || quote.lastUpdated || ''; } export function recentQuoteTimestamp(quote: { createdAt?: string; lastUpdated?: string }): number { const value = recentQuoteDate(quote); const parsed = value ? new Date(value).getTime() : NaN; return Number.isNaN(parsed) ? 0 : parsed; }