import { useState, useMemo } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFileImport, faCheck, faSpinner, faTriangleExclamation, faCloudArrowUp, faArrowRight } from '@fortawesome/pro-duotone-svg-icons';
import { Dialog, Modal, ModalOverlay } from '@/components/application/modals/modal';
import { Button } from '@/components/base/buttons/button';
import { Badge } from '@/components/base/badges/badges';
import { Select } from '@/components/base/select/select';
import { DynamicTable } from '@/components/application/table/dynamic-table';
import type { DynamicColumn, DynamicRow } from '@/components/application/table/dynamic-table';
import { FeaturedIcon } from '@/components/foundations/featured-icon/featured-icon';
import { useData } from '@/providers/data-provider';
import { apiClient } from '@/lib/api-client';
import { parseCSV, fuzzyMatchColumns, buildLeadPayload, normalizePhone, LEAD_FIELDS } from '@/lib/csv-utils';
import { cx } from '@/utils/cx';
import type { Campaign } from '@/types/entities';
import type { LeadFieldMapping, CSVRow } from '@/lib/csv-utils';
import type { FC } from 'react';
const FileImportIcon: FC<{ className?: string }> = ({ className }) => (
);
type ImportStep = 'select-campaign' | 'map-columns' | 'preview' | 'importing' | 'done';
const WIZARD_STEPS = [
{ key: 'select-campaign', label: 'Select Campaign', number: 1 },
{ key: 'map-columns', label: 'Upload & Map', number: 2 },
{ key: 'preview', label: 'Preview', number: 3 },
{ key: 'done', label: 'Import', number: 4 },
] as const;
const StepIndicator = ({ currentStep }: { currentStep: ImportStep }) => {
const activeIndex = currentStep === 'importing'
? 3
: WIZARD_STEPS.findIndex(s => s.key === currentStep);
return (
{WIZARD_STEPS.map((step, i) => {
const isComplete = i < activeIndex;
const isActive = i === activeIndex;
const isLast = i === WIZARD_STEPS.length - 1;
return (
{isComplete ? : step.number}
{step.label}
{!isLast && (
)}
);
})}
);
};
type ImportResult = {
created: number;
linkedToPatient: number;
skippedDuplicate: number;
skippedNoPhone: number;
failed: number;
total: number;
};
interface LeadImportWizardProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}
const PAGE_SIZE = 15;
export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps) => {
const { campaigns, leads, patients, refresh } = useData();
const [step, setStep] = useState('select-campaign');
const [selectedCampaign, setSelectedCampaign] = useState(null);
const [csvRows, setCsvRows] = useState([]);
const [mapping, setMapping] = useState([]);
const [result, setResult] = useState(null);
const [importProgress, setImportProgress] = useState(0);
const [previewPage, setPreviewPage] = useState(1);
const activeCampaigns = useMemo(() =>
campaigns.filter(c => c.campaignStatus === 'ACTIVE' || c.campaignStatus === 'PAUSED'),
[campaigns],
);
const handleClose = () => {
onOpenChange(false);
setTimeout(() => {
setStep('select-campaign');
setSelectedCampaign(null);
setCsvRows([]);
setMapping([]);
setResult(null);
setImportProgress(0);
setPreviewPage(1);
}, 300);
};
const handleFileUpload = (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result as string;
const { rows, headers } = parseCSV(text);
setCsvRows(rows);
setMapping(fuzzyMatchColumns(headers));
};
reader.readAsText(file);
};
const handleMappingChange = (csvHeader: string, leadField: string | null) => {
setMapping(prev => prev.map(m =>
m.csvHeader === csvHeader ? { ...m, leadField, label: leadField ? LEAD_FIELDS.find(f => f.field === leadField)?.label ?? '' : '' } : m,
));
};
// Patient matching
const rowsWithMatch = useMemo(() => {
const phoneMapping = mapping.find(m => m.leadField === 'contactPhone');
if (!phoneMapping || csvRows.length === 0) return [];
const existingLeadPhones = new Set(
leads.map(l => normalizePhone(l.contactPhone?.[0]?.number ?? '')).filter(p => p.length === 10),
);
const patientByPhone = new Map(
patients.filter(p => p.phones?.primaryPhoneNumber).map(p => [normalizePhone(p.phones!.primaryPhoneNumber), p]),
);
return csvRows.map(row => {
const rawPhone = row[phoneMapping.csvHeader] ?? '';
const phone = normalizePhone(rawPhone);
const matchedPatient = phone.length === 10 ? patientByPhone.get(phone) : null;
const isDuplicate = phone.length === 10 && existingLeadPhones.has(phone);
const hasPhone = phone.length === 10;
return { row, phone, matchedPatient, isDuplicate, hasPhone };
});
}, [csvRows, mapping, leads, patients]);
const phoneIsMapped = mapping.some(m => m.leadField === 'contactPhone');
const validCount = rowsWithMatch.filter(r => r.hasPhone && !r.isDuplicate).length;
const duplicateCount = rowsWithMatch.filter(r => r.isDuplicate).length;
const noPhoneCount = rowsWithMatch.filter(r => !r.hasPhone).length;
const patientMatchCount = rowsWithMatch.filter(r => r.matchedPatient).length;
const totalPreviewPages = Math.max(1, Math.ceil(rowsWithMatch.length / PAGE_SIZE));
const handleImport = async () => {
if (!selectedCampaign) return;
setStep('importing');
const importResult: ImportResult = { created: 0, linkedToPatient: 0, skippedDuplicate: 0, skippedNoPhone: 0, failed: 0, total: rowsWithMatch.length };
for (let i = 0; i < rowsWithMatch.length; i++) {
const { row, isDuplicate, hasPhone, matchedPatient } = rowsWithMatch[i];
if (!hasPhone) { importResult.skippedNoPhone++; setImportProgress(i + 1); continue; }
if (isDuplicate) { importResult.skippedDuplicate++; setImportProgress(i + 1); continue; }
const payload = buildLeadPayload(row, mapping, selectedCampaign.id, matchedPatient?.id ?? null, selectedCampaign.platform);
if (!payload) { importResult.skippedNoPhone++; setImportProgress(i + 1); continue; }
try {
await apiClient.graphql(`mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`, { data: payload }, { silent: true });
importResult.created++;
if (matchedPatient) importResult.linkedToPatient++;
} catch { importResult.failed++; }
setImportProgress(i + 1);
}
setResult(importResult);
setStep('done');
refresh();
};
// Select dropdown items for mapping
const mappingOptions = [
{ id: '__skip__', label: '— Skip —' },
...LEAD_FIELDS.map(f => ({ id: f.field, label: f.label })),
];
return (
{ if (!open) handleClose(); }}>
);
};