Files
helix-engage/docs/superpowers/specs/2026-03-31-csv-lead-import-design.md
2026-03-31 11:45:04 +05:30

6.4 KiB

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:
    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