mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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:
165
docs/superpowers/specs/2026-03-31-csv-lead-import-design.md
Normal file
165
docs/superpowers/specs/2026-03-31-csv-lead-import-design.md
Normal 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
|
||||
Reference in New Issue
Block a user