docs: CSV lead import spec + defect fixing plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 11:45:04 +05:30
parent c3c3f4b3d7
commit 5da4c47908
2 changed files with 377 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
# CSV Lead Import — Design Spec
**Date**: 2026-03-31
**Status**: Approved
---
## Overview
Supervisors can import leads from a CSV file into an existing campaign. The feature is a modal wizard accessible from the Campaigns page. Leads are created via the platform GraphQL API and linked to the selected campaign. Existing patients are detected by phone number matching.
---
## User Flow
### Entry Point
"Import Leads" button on the Campaigns page (`/campaigns`). Admin role only.
### Step 1 — Select Campaign (modal opens)
- Campaign cards in a grid layout inside the modal
- Each card shows: campaign name, platform badge (Facebook/Google/Instagram/Manual), status badge (Active/Paused/Completed), lead count
- Click a card to select → proceeds to Step 2
- Only ACTIVE and PAUSED campaigns shown (not COMPLETED)
### Step 2 — Upload & Preview
- File drop zone at top of modal (accepts `.csv` only)
- On file upload, parse CSV client-side
- Show preview table with:
- **Column mapping row**: each CSV column header has a dropdown to map to a Lead field. Fuzzy auto-match on load (e.g., "Phone" → contactPhone, "Name" → contactName.firstName, "Email" → contactEmail, "Service" → interestedService)
- **Data rows**: all rows displayed (paginated at 20 per page if large file)
- **Patient match column** (rightmost): for each row, check phone against existing patients in DataProvider
- Green badge: "Existing — {Patient Name}" (phone matched)
- Gray badge: "New" (no match)
- **Duplicate lead column**: check phone against existing leads
- Orange badge: "Duplicate" (phone already exists as a lead)
- No badge if clean
- Validation:
- `contactPhone` mapping is required — show error banner if unmapped
- Rows with empty phone values are flagged as "Skip — no phone"
- Footer shows summary: "48 leads ready, 3 existing patients, 2 duplicates, 1 skipped"
- "Import" button enabled only when contactPhone is mapped and at least 1 valid row exists
### Step 3 — Import Progress
- "Import" button triggers sequential lead creation
- Progress bar: "Importing 12 / 48..."
- Each lead created via GraphQL mutation:
```graphql
mutation($data: LeadCreateInput!) {
createLead(data: $data) { id }
}
```
- Data payload per lead:
- `name`: "{firstName} {lastName}" or phone if no name
- `contactName`: `{ firstName, lastName }` from mapped columns
- `contactPhone`: `{ primaryPhoneNumber }` from mapped column (normalized with +91 prefix)
- `contactEmail`: `{ primaryEmail }` if mapped
- `interestedService`: if mapped
- `source`: campaign platform (FACEBOOK_AD, GOOGLE_AD, etc.) or MANUAL
- `status`: NEW
- `campaignId`: selected campaign ID
- `patientId`: if phone matched an existing patient
- All other mapped fields set accordingly
- Duplicate leads (phone already exists) are skipped
- On complete: summary card — "45 created, 3 linked to existing patients, 2 skipped (duplicates), 1 skipped (no phone)"
### Step 4 — Done
- Summary with green checkmark
- "Done" button closes modal
- Campaigns page refreshes to show updated lead count
---
## Column Mapping — Fuzzy Match Rules
CSV headers are normalized (lowercase, trim, remove special chars) and matched against Lead field labels:
| CSV Header Pattern | Maps To | Field Type |
|---|---|---|
| name, first name, patient name | contactName.firstName | FULL_NAME |
| last name, surname | contactName.lastName | FULL_NAME |
| phone, mobile, contact number, cell | contactPhone | PHONES |
| email, email address | contactEmail | EMAILS |
| service, interested in, department, specialty | interestedService | TEXT |
| priority | priority | SELECT |
| source, lead source, channel | source | SELECT |
| notes, comments, remarks | (stored as lead name suffix or skipped) | — |
| utm_source, utm_medium, utm_campaign, utm_term, utm_content | utmSource/utmMedium/utmCampaign/utmTerm/utmContent | TEXT |
Unmapped columns are ignored. User can override any auto-match via dropdown.
---
## Phone Normalization
Before matching and creating:
1. Strip all non-digit characters
2. Remove leading `+91` or `91` if 12+ digits
3. Take last 10 digits
4. Store as `+91{10digits}` on the Lead
---
## Patient Matching
Uses the `patients` array from DataProvider (already loaded in memory):
- For each CSV row, normalize the phone number
- Check against `patient.phones.primaryPhoneNumber` (last 10 digits)
- If match found: set `patientId` on the created Lead, show patient name in preview
- If no match: leave `patientId` null, caller resolution will handle it on first call
---
## Duplicate Lead Detection
Uses the `leads` array from DataProvider:
- For each CSV row, check normalized phone against existing `lead.contactPhone[0].number`
- If match found: mark as duplicate in preview, skip during import
- If no match: create normally
---
## Error Handling
- Invalid CSV (no headers, empty file): show error banner in modal, no preview
- File too large (>5000 rows): show warning, allow import but warn about duration
- Individual mutation failures: log error, continue with remaining rows, show count in summary
- Network failure mid-import: show partial result — "23 of 48 imported, import interrupted"
---
## Architecture
### No new sidecar endpoint needed
CSV parsing happens client-side. Lead creation uses the existing GraphQL proxy (`/graphql` → platform). Patient/lead matching uses DataProvider data already in memory.
### Files
| File | Action |
|---|---|
| `src/components/campaigns/lead-import-wizard.tsx` | **New** — Modal wizard component (Steps 1-4) |
| `src/pages/campaigns.tsx` | **Modified** — Add "Import Leads" button |
| `src/lib/csv-parser.ts` | **New** — CSV parsing + column fuzzy matching utility |
### Dependencies
- No new npm packages needed — `FileReader` API + string split for CSV parsing (or use existing `papaparse` if already in node_modules)
- Untitled UI components: Modal, Button, Badge, Table, Input (file), FeaturedIcon
---
## Scope Boundaries
**In scope:**
- Campaign selection via cards
- CSV upload and client-side parsing
- Fuzzy column mapping with manual override
- Preview with patient match + duplicate detection
- Sequential lead creation with progress
- Phone normalization
**Out of scope (future):**
- Dynamic campaign-specific entity creation (AI-driven schema)
- Campaign content/template creation
- Bulk update of existing leads from CSV
- API-based lead ingestion (Facebook/Google webhooks)
- Code generation webhook on schema changes