export type CsvRow = Record; export type ParsedCsv = { header: string[]; rows: CsvRow[] }; export type CustomerInput = { name: string; email: string; company: string; status: string }; export type ImportResult = { total: number; valid: CustomerInput[]; errors: { row: number; message: string }[]; duplicatesInFile: number[]; }; const KNOWN_STATUSES = ['active', 'lead', 'churned']; /** Minimal RFC4180-ish CSV parser: quoted fields, escaped quotes, CRLF. */ export function parseCsv(text: string): ParsedCsv { const rows: string[][] = []; let field = ''; let row: string[] = []; let inQuotes = false; const src = text.replace(/\r\n?/g, '\n'); for (let i = 0; i < src.length; i++) { const ch = src[i]; if (inQuotes) { if (ch === '"') { if (src[i + 1] === '"') { field += '"'; i++; } else inQuotes = false; } else field += ch; } else if (ch === '"') { inQuotes = true; } else if (ch === ',') { row.push(field); field = ''; } else if (ch === '\n') { row.push(field); rows.push(row); row = []; field = ''; } else field += ch; } if (field.length > 0 || row.length > 0) { row.push(field); rows.push(row); } const cleaned = rows.filter((r) => r.some((c) => c.trim() !== '')); const first = cleaned[0]; if (!first) return { header: [], rows: [] }; const header = first.map((h) => h.trim().toLowerCase()); const out: CsvRow[] = cleaned.slice(1).map((r) => { const obj: CsvRow = {}; header.forEach((h, idx) => (obj[h] = (r[idx] ?? '').trim())); return obj; }); return { header, rows: out }; } const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; /** Validate parsed rows for the customer import: required columns, email shape, status enum. */ export function validateCustomerRows(rows: CsvRow[]): ImportResult { const errors: { row: number; message: string }[] = []; const valid: CustomerInput[] = []; const seen = new Map(); const duplicatesInFile: number[] = []; rows.forEach((row, idx) => { const line = idx + 2; // 1-based + header const name = (row.name ?? '').trim(); const email = (row.email ?? '').trim().toLowerCase(); const company = (row.company ?? '').trim(); const status = (row.status ?? 'active').trim().toLowerCase() || 'active'; if (!name) { errors.push({ row: line, message: 'name is required' }); return; } if (!email || !EMAIL_RE.test(email)) { errors.push({ row: line, message: `invalid email "${row.email ?? ''}"` }); return; } if (!KNOWN_STATUSES.includes(status)) { errors.push({ row: line, message: `unknown status "${status}" (use ${KNOWN_STATUSES.join('/')})` }); return; } const priorLine = seen.get(email); if (priorLine !== undefined) { duplicatesInFile.push(line); return; } seen.set(email, line); valid.push({ name, email, company, status }); }); return { total: rows.length, valid, errors, duplicatesInFile }; } export function toCsv(rows: Record[], columns: string[]): string { const escape = (v: unknown) => { const s = v === null || v === undefined ? '' : String(v); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; return [columns.join(','), ...rows.map((r) => columns.map((c) => escape(r[c])).join(','))].join('\n'); }