# Helix Engage — Frontend Call center CRM frontend for healthcare lead management. Built on the FortyTwo platform. **Owner: Mouli** ## Architecture ``` ┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │ helix-engage │ │ helix-engage-server │ │ FortyTwo Platform │ │ (this repo) │────▶│ (sidecar) │────▶│ (backend) │ │ React frontend │ │ NestJS REST API │ │ GraphQL API │ │ Port 5173 (dev) │ │ Port 4100 │ │ Port 4000 │ └─────────────────────┘ └──────────────────────┘ └─────────────────────┘ │ │ │ SIP/WebRTC │ Ozonetel CloudAgent APIs ▼ ▼ ┌───────────┐ ┌──────────────┐ │ Ozonetel │ │ Ozonetel │ │ SIP (444) │ │ REST APIs │ └───────────┘ └──────────────┘ ``` **Three repos:** | Repo | Purpose | Owner | |------|---------|-------| | `helix-engage` (this) | React frontend | Mouli | | `helix-engage-server` | NestJS sidecar — Ozonetel + Platform bridge | Karthik | | `helix-engage-app` | FortyTwo SDK app — entity schemas (Call, Lead, etc.) | Shared | ## Getting Started ```bash npm install npm run dev # http://localhost:5173 npm run build # TypeScript check + production build ``` ### Environment Variables (set at build time or in `.env`) | Variable | Purpose | Dev Default | Production | | -------------------- | ---------------- | ----------------------- | ------------------------------------------- | | `VITE_API_URL` | Platform GraphQL | `http://localhost:4000` | `https://engage-api.srv1477139.hstgr.cloud` | | `VITE_SIDECAR_URL` | Sidecar REST API | `http://localhost:4100` | `https://engage-api.srv1477139.hstgr.cloud` | | `VITE_SIP_URI` | Ozonetel SIP URI | — | `sip:523590@blr-pub-rtc4.ozonetel.com` | | `VITE_SIP_PASSWORD` | SIP password | — | `523590` | | `VITE_SIP_WS_SERVER` | SIP WebSocket | — | `wss://blr-pub-rtc4.ozonetel.com:444` | **Production build command:** ```bash VITE_API_URL=https://engage-api.srv1477139.hstgr.cloud \ VITE_SIDECAR_URL=https://engage-api.srv1477139.hstgr.cloud \ VITE_SIP_URI=sip:523590@blr-pub-rtc4.ozonetel.com \ VITE_SIP_PASSWORD=523590 \ VITE_SIP_WS_SERVER=wss://blr-pub-rtc4.ozonetel.com:444 \ npm run build ``` ## Tech Stack - **React 19** + TypeScript + Vite - **Tailwind CSS 4** with semantic color tokens (`text-primary`, `bg-brand-section` — never raw colors like `text-gray-900`) - **React Aria Components** for accessibility (imports always prefixed `Aria*`) - **Jotai** for SIP/call state - **React Context** for auth, data, theme - **FontAwesome Pro Duotone** icons - **Untitled UI** component library (`src/components/base/`, `src/components/application/`) ## Project Structure ``` src/ ├── pages/ # Route-level pages │ ├── call-desk.tsx # Main CC agent workspace — THE CORE PAGE │ ├── login.tsx # Auth page (centered card on blue bg) │ ├── call-history.tsx # CDR log viewer │ ├── my-performance.tsx # Agent KPI dashboard │ ├── team-dashboard.tsx # Supervisor overview │ ├── all-leads.tsx # Lead master table │ └── campaigns.tsx # Campaign listing │ ├── components/ │ ├── call-desk/ # ⚡ Call center components — WHERE MOST WORK HAPPENS │ │ ├── active-call-card.tsx # In-call UI + post-call disposition flow │ │ ├── worklist-panel.tsx # Agent task queue with tabs + sub-tabs │ │ ├── context-panel.tsx # AI assistant + Lead 360 sidebar │ │ ├── disposition-form.tsx # Post-call outcome selector │ │ ├── appointment-form.tsx # Book appointment during/after call │ │ ├── agent-status-toggle.tsx # Ready/Break/Training/Offline toggle │ │ ├── transfer-dialog.tsx # Call transfer │ │ ├── enquiry-form.tsx # General enquiry capture │ │ ├── live-transcript.tsx # Real-time transcription (Deepgram) │ │ └── phone-action-cell.tsx # Click-to-call in table rows │ ├── base/ # Untitled UI primitives (Button, Input, Select, Badge) │ ├── application/ # Complex UI (Table, Modal, Tabs, DatePicker, Nav) │ ├── layout/ # Sidebar — role-based navigation │ └── dashboard/ # KPI cards, charts, missed queue widget │ ├── providers/ │ ├── sip-provider.tsx # SIP WebRTC — call lifecycle management │ ├── auth-provider.tsx # User session, roles (executive/admin/cc-agent) │ ├── data-provider.tsx # Bulk entity loader (leads, campaigns, calls) │ └── theme-provider.tsx # Light/dark mode │ ├── hooks/ │ ├── use-worklist.ts # Polls sidecar /api/worklist every 30s │ ├── use-call-assist.ts # Live transcript via Socket.IO │ └── use-sip-phone.ts # Low-level SIP.js wrapper │ ├── lib/ │ ├── api-client.ts # REST + GraphQL client (auth, queries, sidecar calls) │ ├── queries.ts # Platform GraphQL query strings │ └── format.ts # Phone/date formatting │ ├── state/ │ └── sip-state.ts # Jotai atoms (callState, callerNumber, isMuted, etc.) │ └── types/ └── entities.ts # Lead, Patient, Call, Appointment, etc. ``` ## Troubleshooting Guide — Where to Look ### "The call desk isn't working" **File:** `src/pages/call-desk.tsx` This is the orchestrator. It uses `useSip()` for call state, `useWorklist()` for the task queue, and renders either `ActiveCallCard` (in-call) or `WorklistPanel` (idle). Start here, then drill into whichever child component is misbehaving. ### "Calls aren't connecting / SIP errors" **File:** `src/providers/sip-provider.tsx` + `src/state/sip-state.ts` Check `VITE_SIP_*` env vars. Ozonetel SIP WebSocket runs on **port 444** — VPNs block it. If WebSocket hangs at "connecting", turn off VPN. Also check browser console for SIP.js registration errors. ### "Worklist not loading / empty" **File:** `src/hooks/use-worklist.ts` This polls `GET /api/worklist` on the sidecar every 30s. Open browser Network tab → filter for `/api/worklist`. Common causes: sidecar is down, auth token expired, or agent name doesn't match any assigned leads. ### "Missed calls not appearing / sub-tabs empty" **File:** `src/components/call-desk/worklist-panel.tsx` Missed calls come from the sidecar worklist response. The sub-tabs filter by `callbackstatus` field. If all sub-tabs are empty, the sidecar ingestion may not be running (check sidecar logs for `MissedQueueService`). ### "Disposition / appointment not saving" **File:** `src/components/call-desk/active-call-card.tsx` → `handleDisposition()` Posts to sidecar `POST /api/ozonetel/dispose`. Errors are caught silently (non-blocking). Check browser Network tab for the dispose request/response, then check sidecar logs. ### "Login broken / Failed to fetch" **File:** `src/pages/login.tsx` + `src/lib/api-client.ts` Login calls `apiClient.login()` → sidecar `/auth/login` → platform GraphQL. Most common cause: wrong `VITE_API_URL` (built with localhost instead of production URL). **Always set env vars at build time.** ### "UI component looks wrong" **Files:** `src/components/base/` (primitives), `src/components/application/` (complex) These come from the Untitled UI library. Design tokens are in `src/styles/theme.css`. Brand colors were rebuilt from logo blue `rgb(32, 96, 160)`. ### "Navigation / role-based access" **File:** `src/components/layout/sidebar.tsx` Navigation groups are defined per role (admin, cc-agent, executive). Routes are registered in `src/main.tsx`. ## Data Flow ``` User action │ ▼ Component (e.g. ActiveCallCard) │ ├──▶ Sidecar REST API (via apiClient.post/get) │ e.g. /api/ozonetel/dispose, /api/worklist │ ├──▶ Platform GraphQL (via apiClient.graphql) │ e.g. leads, appointments, patients queries │ └──▶ SIP.js (via useSip() hook) Call control: answer, hangup, mute, hold ``` **Key pattern:** The frontend talks to TWO backends: 1. **Sidecar** (REST) — for Ozonetel telephony operations and worklist 2. **Platform** (GraphQL) — for entity CRUD (leads, appointments, patients) ## Conventions - **File naming**: kebab-case (`worklist-panel.tsx`) - **Colors**: Semantic tokens only (`text-primary`, `bg-brand-section`) - **Icons**: `@fortawesome/pro-duotone-svg-icons` + `faIcon()` wrapper in `src/lib/icon-wrapper.ts` - **React Aria**: Always prefix imports (`Button as AriaButton`) - **Transitions**: `transition duration-100 ease-linear` ## Git Workflow - `dev` — active development - `master` — stable baseline - Always build with production env vars before deploying