64 lines
3.2 KiB
TypeScript
64 lines
3.2 KiB
TypeScript
/* ── 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<string, any> = {};
|
|
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;
|
|
}
|