diff --git a/src/components/campaigns/lead-import-wizard.tsx b/src/components/campaigns/lead-import-wizard.tsx
index 08efaf1..36c6b07 100644
--- a/src/components/campaigns/lead-import-wizard.tsx
+++ b/src/components/campaigns/lead-import-wizard.tsx
@@ -1,9 +1,10 @@
import { useState, useMemo } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faFileImport, faCheck, faSpinner, faTriangleExclamation, faCloudArrowUp } from '@fortawesome/pro-duotone-svg-icons';
+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';
@@ -19,16 +20,19 @@ const FileImportIcon: FC<{ className?: string }> = ({ className }) => (
);
-type ImportStep = 'select-campaign' | 'upload-preview' | 'importing' | 'done';
+type ImportStep = 'select-campaign' | 'map-columns' | 'preview' | 'importing' | 'done';
const WIZARD_STEPS = [
{ key: 'select-campaign', label: 'Select Campaign', number: 1 },
- { key: 'upload-preview', label: 'Upload & Map', number: 2 },
- { key: 'done', label: 'Import', number: 3 },
+ { 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' ? 2 : WIZARD_STEPS.findIndex(s => s.key === currentStep);
+ const activeIndex = currentStep === 'importing'
+ ? 3
+ : WIZARD_STEPS.findIndex(s => s.key === currentStep);
return (
@@ -82,6 +86,8 @@ interface LeadImportWizardProps {
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');
@@ -90,6 +96,7 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
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'),
@@ -105,18 +112,13 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
setMapping([]);
setResult(null);
setImportProgress(0);
+ setPreviewPage(1);
}, 300);
};
- const handleCampaignSelect = (campaign: Campaign) => {
- setSelectedCampaign(campaign);
- setStep('upload-preview');
- };
-
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;
@@ -133,6 +135,7 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
));
};
+ // Patient matching
const rowsWithMatch = useMemo(() => {
const phoneMapping = mapping.find(m => m.leadField === 'contactPhone');
if (!phoneMapping || csvRows.length === 0) return [];
@@ -140,11 +143,8 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
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]),
+ patients.filter(p => p.phones?.primaryPhoneNumber).map(p => [normalizePhone(p.phones!.primaryPhoneNumber), p]),
);
return csvRows.map(row => {
@@ -153,7 +153,6 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
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]);
@@ -163,6 +162,7 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
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;
@@ -172,7 +172,6 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
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; }
@@ -180,17 +179,10 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
if (!payload) { importResult.skippedNoPhone++; setImportProgress(i + 1); continue; }
try {
- await apiClient.graphql(
- `mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`,
- { data: payload },
- { silent: true },
- );
+ await apiClient.graphql(`mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`, { data: payload }, { silent: true });
importResult.created++;
if (matchedPatient) importResult.linkedToPatient++;
- } catch {
- importResult.failed++;
- }
-
+ } catch { importResult.failed++; }
setImportProgress(i + 1);
}
@@ -199,6 +191,12 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
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(); }}>
@@ -213,7 +211,8 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
Import Leads
{step === 'select-campaign' && 'Select a campaign to import leads into'}
- {step === 'upload-preview' && `Importing into: ${selectedCampaign?.campaignName}`}
+ {step === 'map-columns' && 'Upload CSV and map columns to lead fields'}
+ {step === 'preview' && `Preview: ${selectedCampaign?.campaignName}`}
{step === 'importing' && 'Importing leads...'}
{step === 'done' && 'Import complete'}
@@ -222,21 +221,21 @@ export const LeadImportWizard = ({ isOpen, onOpenChange }: LeadImportWizardProps
- {/* Step indicator */}
{/* Content */}
-
+
+
{/* Step 1: Campaign Cards */}
{step === 'select-campaign' && (
-
- {activeCampaigns.length === 0 ? (
-
No active campaigns. Create a campaign first.
- ) : (
- activeCampaigns.map(campaign => (
+
+
+ {activeCampaigns.length === 0 ? (
+
No active campaigns.
+ ) : activeCampaigns.map(campaign => (
- ))
- )}
+ ))}
+
)}
- {/* Step 2: Upload + Preview */}
- {step === 'upload-preview' && (
-
+ {/* Step 2: Upload + Column Mapping */}
+ {step === 'map-columns' && (
+
{csvRows.length === 0 ? (
-