From 5da4c47908bc7d0cf8c9e438a9aff0ed4ae3a3c3 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Tue, 31 Mar 2026 11:45:04 +0530 Subject: [PATCH] docs: CSV lead import spec + defect fixing plan Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/defect-fixing-plan.md | 212 ++++++++++++++++++ .../2026-03-31-csv-lead-import-design.md | 165 ++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 docs/defect-fixing-plan.md create mode 100644 docs/superpowers/specs/2026-03-31-csv-lead-import-design.md diff --git a/docs/defect-fixing-plan.md b/docs/defect-fixing-plan.md new file mode 100644 index 0000000..db19521 --- /dev/null +++ b/docs/defect-fixing-plan.md @@ -0,0 +1,212 @@ +# Helix Engage — Defect Fixing Plan + +**Date**: 2026-03-31 +**Status**: Analysis complete, implementation pending + +--- + +## Item 1: Sidebar navigation during ongoing calls +**Status**: NOT A BUG +**Finding**: Sidebar is fully functional during calls. No code blocks navigation. Call state persists via Jotai atoms (`sipCallStateAtom`, `sipCallerNumberAtom`, `sipCallUcidAtom`) regardless of which page the agent navigates to. `CallWidget` in `app-shell.tsx` (line 80) renders on non-call-desk pages when a call is active, ensuring the agent can return. + +--- + +## Item 2: Appointment form / Enquiry form visibility during calls +**Status**: APPROVED REDESIGN — Convert to modals +**Root Cause**: `active-call-card.tsx` renders AppointmentForm and EnquiryForm inside a `max-h-[50vh] overflow-y-auto` container (line 292). After the call header + controls take ~100px, the form is squeezed. + +**Approved approach**: Convert both forms to modal dialogs (like TransferDialog already is). + +**Flow**: +``` +Agent clicks "Book Appt" → Modal opens → Log intent to LeadActivity → Agent fills form + → Save succeeds → setSuggestedDisposition('APPOINTMENT_BOOKED') → Modal closes + → Save abandoned → No disposition change → Intent logged for supervisor analytics +``` + +Same for Enquiry → `INFO_PROVIDED` on save, intent logged on open. + +**Files to change**: +- `src/components/call-desk/active-call-card.tsx` — replace inline form expansion with modal triggers +- `src/components/call-desk/appointment-form.tsx` — wrap in Modal/ModalOverlay from `src/components/application/modals/modal` +- `src/components/call-desk/enquiry-form.tsx` — wrap in Modal/ModalOverlay + +**Benefits**: Solves Item 2 (form visibility), Item 10a (returning patient checkbox shift), keeps call card clean. + +**Effort**: Medium (3-4h) + +--- + +## Item 3: Enquiry form disposition + modal disposition context +**Status**: REAL ISSUE (two parts) + +### 3a: Remove disposition from enquiry form +`enquiry-form.tsx` (lines 19-26, 195-198) has its own disposition field with 6 options (CONVERTED, FOLLOW_UP, GENERAL_QUERY, NO_ANSWER, INVALID_NUMBER, CALL_DROPPED). During an active call, NO_ANSWER and INVALID_NUMBER are nonsensical — the caller is connected. + +**Fix**: Remove disposition field from enquiry form entirely. Disposition is captured in the disposition modal after the call ends. The enquiry form's job is to log the enquiry, not to classify the call outcome. + +**Files**: `src/components/call-desk/enquiry-form.tsx` — remove disposition Select + validation + +### 3b: Context-aware disposition options in modal +`disposition-modal.tsx` (lines 15-57) shows all 6 options regardless of call context. During an inbound answered call, "No Answer" and "Wrong Number" don't apply. + +**Fix**: Accept a `callContext` prop ('inbound-answered' | 'outbound' | 'missed-callback') and filter options accordingly: +- Inbound answered: show APPOINTMENT_BOOKED, FOLLOW_UP_SCHEDULED, INFO_PROVIDED, CALLBACK_REQUESTED +- Outbound: show all +- Missed callback: show all + +**Files**: `src/components/call-desk/disposition-modal.tsx`, `src/components/call-desk/active-call-card.tsx` + +**Effort**: Low (2h) + +--- + +## Item 4: Edit future appointment during inbound call +**Status**: DONE (2026-03-30) +**Implementation**: Context panel (`context-panel.tsx` lines 172-197) shows upcoming appointments with Edit button → opens `AppointmentForm` in edit mode with `existingAppointment` prop. Appointments fetched via `APPOINTMENTS_QUERY` in DataProvider. + +--- + +## Item 5: My Performance page +**Status**: THREE SUB-ISSUES + +### 5a: From/To date range filter +**Current**: Only Today/Yesterday presets + single date picker in `my-performance.tsx` (lines 95-135). +**Fix**: Add two DatePicker components (From/To) or a date range picker. Update API call to accept date range. Update chart/KPI computations to use range. +**Effort**: Medium (3-4h) + +### 5b: Time Utilisation not displayed +**Current**: Section renders conditionally at line 263 — only if `timeUtilization` is not null. If sidecar API returns null (Ozonetel getAgentSummary fails or VPN blocks), section silently disappears. +**Fix**: Add placeholder/error state when null: "Time utilisation data unavailable — check Ozonetel connection" +**Effort**: Low (30min) + +### 5c: Data loading slow +**Current**: Fetches from `/api/ozonetel/performance` on every date change, no caching. +**Fix**: Add response caching (memoize by date key), show skeleton loader during fetch, debounce date changes. +**Effort**: Medium (2h) + +--- + +## Item 6: Break and Training status not working +**Status**: REAL ISSUE — likely Ozonetel API parameter mismatch + +**Root Cause**: `agent-status-toggle.tsx` (lines 41-64) calls `/api/ozonetel/agent-state` with `{ state: 'Pause', pauseReason: 'Break' }` or `'Training'`. Ozonetel's `changeAgentState` API may expect different pause reason enum values. Errors are caught and shown as generic toast — no specific failure reason. + +**Investigation needed**: +1. Check sidecar logs for the actual Ozonetel API response when Break/Training is selected +2. Verify Ozonetel API docs for valid `pauseReason` values (may need `BREAK`, `TRAINING`, or numeric codes) +3. Check if the agent must be in `Ready` state before transitioning to `Pause` + +**Fix**: Correct pause reason values, add specific error messages. +**Effort**: Low-Medium (2-3h including investigation) + +--- + +## Item 7: Auto-refresh for Call Desk, Call History, Appointments +**Status**: REAL ISSUE + +| Page | Current | Fix | +|---|---|---| +| Call Desk worklist | YES (30s via `use-worklist.ts`) | Working | +| DataProvider (calls, leads, etc.) | NO — `useEffect([fetchData])` runs once | Add `setInterval(fetchData, 30000)` | +| Call History | NO — uses `useData()` | Automatic once DataProvider fixed | +| Appointments | NO — `useEffect([])` runs once | Add interval or move to DataProvider | + +**Files**: `src/providers/data-provider.tsx` (lines 117-119), `src/pages/appointments.tsx` (lines 76-81) +**Effort**: Low (1-2h) + +--- + +## Item 8: Appointments page improvements +**Status**: THREE SUB-ISSUES + +### 8a: Appointment ID as primary field +**Current**: No ID column in table. `appointments.tsx` shows Patient, Date, Time, Doctor, Department, Branch, Status, Chief Complaint. +**Fix**: Add ID column (first column) showing appointment ID or a short reference number. +**Effort**: Low (30min) + +### 8b: Edit Appointment option +**Current**: No edit button on appointments page (only exists in call desk context panel). +**Fix**: Add per-row Edit button → opens AppointmentForm in edit mode (same component, reuse `existingAppointment` prop). +**Pending**: Confirmation from Meghana +**Effort**: Low (1-2h) + +### 8c: Sort by status +**Current**: Tabs filter by status but no column-level sorting. +**Fix**: Add `allowsSorting` to table headers + `sortDescriptor`/`onSortChange` (same pattern as worklist). +**Pending**: Confirmation from Meghana +**Effort**: Low (1h) + +--- + +## Item 9: AI Surface enlargement + patient historical data +**Status**: PARTIALLY DONE + +### 9a: Panel width +**Current**: Context panel is `w-[400px]` in `call-desk.tsx` (line 218). +**Fix**: Increase to `w-[440px]` or `w-[460px]`. +**Effort**: Trivial + +### 9b: Patient historical data +**Current**: We added calls, follow-ups, and appointments to context panel (2026-03-30). Shows in "Upcoming" and "Recent" sections. Data requires `patientId` on the lead — populated by caller resolution service. +**Verify**: Test with real inbound call to confirmed patient. If lead has no `patientId`, nothing shows. +**Effort**: Done — verify only + +--- + +## Item 10: Multiple issues + +### 10a: Returning Patient checkbox shifts form upward +**Status**: WILL BE FIXED by Item 2 (modal conversion). Form in modal has its own layout — checkbox toggle won't affect call card. + +### 10b: Patients page table not scrollable +**File**: `src/pages/patients.tsx` +**Fix**: Add `overflow-auto` to table container wrapper. Check if outer div has proper `min-h-0` for flex overflow. +**Effort**: Trivial (15min) + +### 10c: Call log data not appearing in worklist tabs +**Status**: INVESTIGATION NEEDED +**Possible causes**: +1. Sidecar `/api/worklist` not returning data — check endpoint response +2. Calls created via Ozonetel disposition lack `leadId` linkage — can't match to worklist +3. Call records created but `callStatus` not set correctly (need `MISSED` for missed tab) +**Action**: Check sidecar logs and `/api/worklist` response payload + +### 10d: Missed calls appearing in wrong sub-tabs (Attempted/Completed/Invalid instead of Pending) +**Status**: INVESTIGATION NEEDED +**Possible cause**: `callbackstatus` field being set to non-null value during call creation. `worklist-panel.tsx` (line 246) routes to Pending when `callbackstatus === 'PENDING_CALLBACK' || !callbackstatus`. If the sidecar sets a status during ingestion, it may skip Pending. +**Action**: Check missed call ingestion code in sidecar — what `callbackstatus` is set on creation + +--- + +## Item 11: Patient column filter in Call Desk +**Status**: NOT A BUG +**Finding**: The PATIENT column has `allowsSorting` (added 2026-03-30) which shows a sort arrow. This is a sort control, not a filter. The search box at the top of the worklist filters across name + phone. No separate column-level filter exists. Functionally correct. + +--- + +## Priority Matrix + +| Priority | Items | Total Effort | +|---|---|---| +| **P0 — Do first** | #2 (modal conversion — solves 2, 10a), #7 (auto-refresh), #3 (disposition context) | ~7h | +| **P1 — Quick wins** | #8a (appt ID), #8c (sort), #9a (panel width), #10b (scroll fix), #5b (time util placeholder) | ~3h | +| **P2 — Medium** | #5a (date range), #5c (loading perf), #6 (break/training debug), #8b (edit appt) | ~8h | +| **P3 — Investigation** | #10c (call log data), #10d (missed call routing) | ~2h investigation | +| **Done** | #1, #4, #9b, #11 | — | + +## Data Seeding (separate from defects) + +### Patient/Lead seeding +| Name | Phone | Action | +|---|---|---| +| Ganesh Bandi | 8885540404 | Create patient + lead, interestedService: "Back Pain" | +| Meghana | 7702055204 | Update existing "Unknown" patient + lead, interestedService: "Hair Loss" | + +### CC Agent profiles (completed) +``` +Agent Email Password Ozonetel ID SIP Ext Campaign +-------- ---------------------------- --------- -------------- -------- ---------------------- +Rekha S rekha.cc@globalhospital.com Test123$ global 523590 Inbound_918041763265 +Ganesh ganesh.cc@globalhospital.com Test123$ globalhealthx 523591 Inbound_918041763265 +``` diff --git a/docs/superpowers/specs/2026-03-31-csv-lead-import-design.md b/docs/superpowers/specs/2026-03-31-csv-lead-import-design.md new file mode 100644 index 0000000..12b4afd --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-csv-lead-import-design.md @@ -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