diff --git a/src/lib/csv-utils.ts b/src/lib/csv-utils.ts new file mode 100644 index 0000000..4484735 --- /dev/null +++ b/src/lib/csv-utils.ts @@ -0,0 +1,143 @@ +export type CSVRow = Record; + +export type CSVParseResult = { + headers: string[]; + rows: CSVRow[]; +}; + +export const parseCSV = (text: string): CSVParseResult => { + const lines = text.split(/\r?\n/).filter(line => line.trim()); + if (lines.length === 0) return { headers: [], rows: [] }; + + const parseLine = (line: string): string[] => { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + if (char === '"') { + if (inQuotes && line[i + 1] === '"') { + current += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + result.push(current.trim()); + current = ''; + } else { + current += char; + } + } + result.push(current.trim()); + return result; + }; + + const headers = parseLine(lines[0]); + const rows = lines.slice(1).map(line => { + const values = parseLine(line); + const row: CSVRow = {}; + headers.forEach((header, i) => { + row[header] = values[i] ?? ''; + }); + return row; + }); + + return { headers, rows }; +}; + +export const normalizePhone = (raw: string): string => { + const digits = raw.replace(/\D/g, ''); + const stripped = digits.length >= 12 && digits.startsWith('91') ? digits.slice(2) : digits; + return stripped.slice(-10); +}; + +export type LeadFieldMapping = { + csvHeader: string; + leadField: string | null; + label: string; +}; + +const LEAD_FIELDS = [ + { field: 'contactName.firstName', label: 'First Name', patterns: ['first name', 'firstname', 'name', 'patient name', 'patient'] }, + { field: 'contactName.lastName', label: 'Last Name', patterns: ['last name', 'lastname', 'surname'] }, + { field: 'contactPhone', label: 'Phone', patterns: ['phone', 'mobile', 'contact number', 'cell', 'phone number', 'mobile number'] }, + { field: 'contactEmail', label: 'Email', patterns: ['email', 'email address', 'mail'] }, + { field: 'interestedService', label: 'Interested Service', patterns: ['service', 'interested in', 'department', 'specialty', 'interest'] }, + { field: 'priority', label: 'Priority', patterns: ['priority', 'urgency'] }, + { field: 'utmSource', label: 'UTM Source', patterns: ['utm_source', 'utmsource', 'source'] }, + { field: 'utmMedium', label: 'UTM Medium', patterns: ['utm_medium', 'utmmedium', 'medium'] }, + { field: 'utmCampaign', label: 'UTM Campaign', patterns: ['utm_campaign', 'utmcampaign'] }, + { field: 'utmTerm', label: 'UTM Term', patterns: ['utm_term', 'utmterm', 'term'] }, + { field: 'utmContent', label: 'UTM Content', patterns: ['utm_content', 'utmcontent'] }, +]; + +export const fuzzyMatchColumns = (csvHeaders: string[]): LeadFieldMapping[] => { + const used = new Set(); + + return csvHeaders.map(header => { + const normalized = header.toLowerCase().trim().replace(/[^a-z0-9 ]/g, ''); + let bestMatch: string | null = null; + + for (const field of LEAD_FIELDS) { + if (used.has(field.field)) continue; + if (field.patterns.some(p => normalized === p || normalized.includes(p))) { + bestMatch = field.field; + used.add(field.field); + break; + } + } + + return { + csvHeader: header, + leadField: bestMatch, + label: bestMatch ? LEAD_FIELDS.find(f => f.field === bestMatch)!.label : '', + }; + }); +}; + +export const buildLeadPayload = ( + row: CSVRow, + mapping: LeadFieldMapping[], + campaignId: string, + patientId: string | null, + platform: string | null, +) => { + const getValue = (field: string): string => { + const entry = mapping.find(m => m.leadField === field); + return entry ? (row[entry.csvHeader] ?? '').trim() : ''; + }; + + const firstName = getValue('contactName.firstName') || 'Unknown'; + const lastName = getValue('contactName.lastName'); + const phone = normalizePhone(getValue('contactPhone')); + + if (!phone || phone.length < 10) return null; + + const sourceMap: Record = { + FACEBOOK: 'FACEBOOK_AD', + GOOGLE: 'GOOGLE_AD', + INSTAGRAM: 'INSTAGRAM', + MANUAL: 'OTHER', + }; + + return { + name: `${firstName} ${lastName}`.trim(), + contactName: { firstName, lastName }, + contactPhone: { primaryPhoneNumber: `+91${phone}` }, + ...(getValue('contactEmail') ? { contactEmail: { primaryEmail: getValue('contactEmail') } } : {}), + ...(getValue('interestedService') ? { interestedService: getValue('interestedService') } : {}), + ...(getValue('utmSource') ? { utmSource: getValue('utmSource') } : {}), + ...(getValue('utmMedium') ? { utmMedium: getValue('utmMedium') } : {}), + ...(getValue('utmCampaign') ? { utmCampaign: getValue('utmCampaign') } : {}), + ...(getValue('utmTerm') ? { utmTerm: getValue('utmTerm') } : {}), + ...(getValue('utmContent') ? { utmContent: getValue('utmContent') } : {}), + source: sourceMap[platform ?? ''] ?? 'OTHER', + status: 'NEW', + campaignId, + ...(patientId ? { patientId } : {}), + }; +}; + +export { LEAD_FIELDS };