feat: CC agent features, live call assist, worklist redesign, brand tokens

CC Agent:
- Call transfer (CONFERENCE + KICK_CALL) with inline transfer dialog
- Recording pause/resume during active calls
- Missed calls API (Ozonetel abandonCalls)
- Call history API (Ozonetel fetchCDRDetails)

Live Call Assist:
- Deepgram Nova STT via raw WebSocket
- OpenAI suggestions every 10s with lead context
- LiveTranscript component in sidebar during calls
- Browser audio capture from remote WebRTC stream

Worklist:
- Redesigned table: clickable phones, context menu (Call/SMS/WhatsApp)
- Last interaction sub-line, source column, improved SLA
- Filtered out rows without phone numbers
- New missed call notifications

Brand:
- Logo on login page
- Blue scale rebuilt from logo blue rgb(32, 96, 160)
- FontAwesome duotone CSS variables set globally
- Profile menu icons switched to duotone

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 10:36:10 +05:30
parent 99bca1e008
commit 3064eeb444
21 changed files with 2583 additions and 85 deletions

View File

@@ -0,0 +1,435 @@
# Worklist UX Redesign — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Redesign the call desk worklist table for faster agent action — clickable phone numbers, last interaction context, campaign tags, context menus for SMS/WhatsApp, and meaningful SLA indicators.
**Architecture:** All changes are frontend-only. The data model already has everything needed (`lastContacted`, `contactAttempts`, `source`, `utmCampaign`, `interestedService`, `disposition` on calls). We enrich the worklist rows with this data and redesign the table columns.
**Tech Stack:** React 19, Untitled UI components, FontAwesome Pro Duotone icons, Jotai
---
## Current problems
1. Phone column is passive text — separate Call button in Actions column wastes space
2. No last interaction context — agent doesn't know what happened before
3. No campaign/source — agent can't personalize the opening
4. SLA shows time since creation, not time since last contact
5. Rows without phone numbers are dead weight
6. No way to SMS or WhatsApp from the worklist
## Column redesign
| Before | After |
|--------|-------|
| PRIORITY \| PATIENT \| PHONE \| TYPE \| SLA \| ACTIONS | PRIORITY \| PATIENT \| PHONE \| SOURCE \| SLA |
- **PRIORITY** — badge, same as now
- **PATIENT** — name + sub-line: last interaction context ("Called 2h ago — Info Provided") or interested service
- **PHONE** — clickable number with phone icon. Hover shows context menu (Call / SMS / WhatsApp). On mobile, long-press shows the same menu. No separate Actions column.
- **SOURCE** — campaign/source tag (e.g., "Facebook", "Google", "Walk-in")
- **SLA** — time since `lastContacted` (not `createdAt`). Falls back to `createdAt` if never contacted.
## File map
| File | Responsibility | Action |
|------|---------------|--------|
| `src/components/call-desk/worklist-panel.tsx` | Worklist table + tabs | Modify: redesign columns, add phone context menu, enrich rows |
| `src/components/call-desk/phone-action-cell.tsx` | Clickable phone with context menu | Create: encapsulates call/SMS/WhatsApp actions |
| `src/hooks/use-worklist.ts` | Worklist data fetching | Modify: pass through `lastContacted`, `source`, `utmCampaign` fields |
---
## Task 1: Enrich worklist data with last interaction and source
Pass through the additional fields that already exist in the Lead data but aren't currently used in the worklist row.
**Files:**
- Modify: `helix-engage/src/components/call-desk/worklist-panel.tsx`
- [ ] **Step 1: Extend WorklistLead type in worklist-panel**
Add fields that are already returned by the hook but not typed:
```typescript
type WorklistLead = {
id: string;
createdAt: string;
contactName: { firstName: string; lastName: string } | null;
contactPhone: { number: string; callingCode: string }[] | null;
leadSource: string | null;
leadStatus: string | null;
interestedService: string | null;
aiSummary: string | null;
aiSuggestedAction: string | null;
// New fields (already in API response)
lastContacted: string | null;
contactAttempts: number | null;
utmCampaign: string | null;
campaignId: string | null;
};
```
- [ ] **Step 2: Extend WorklistRow with new fields**
```typescript
type WorklistRow = {
// ... existing fields ...
lastContactedAt: string | null;
contactAttempts: number;
source: string | null; // leadSource or utmCampaign
lastDisposition: string | null;
};
```
- [ ] **Step 3: Populate new fields in buildRows**
For leads:
```typescript
rows.push({
// ... existing ...
lastContactedAt: lead.lastContacted ?? null,
contactAttempts: lead.contactAttempts ?? 0,
source: lead.leadSource ?? lead.utmCampaign ?? null,
lastDisposition: null,
});
```
For missed calls:
```typescript
rows.push({
// ... existing ...
lastContactedAt: call.startedAt ?? call.createdAt,
contactAttempts: 0,
source: null,
lastDisposition: call.disposition ?? null,
});
```
For follow-ups:
```typescript
rows.push({
// ... existing ...
lastContactedAt: fu.scheduledAt ?? fu.createdAt ?? null,
contactAttempts: 0,
source: null,
lastDisposition: null,
});
```
- [ ] **Step 4: Update MissedCall type to include disposition**
The hook already returns `disposition` but the worklist panel type doesn't have it:
```typescript
type MissedCall = {
// ... existing ...
disposition: string | null;
};
```
- [ ] **Step 5: Commit**
```
feat: enrich worklist rows with last interaction and source data
```
---
## Task 2: Create PhoneActionCell component
A reusable cell that shows the phone number as a clickable element with a context menu for Call, SMS, and WhatsApp.
**Files:**
- Create: `helix-engage/src/components/call-desk/phone-action-cell.tsx`
- [ ] **Step 1: Create the component**
```typescript
import { useState, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPhone, faComment, faEllipsisVertical } from '@fortawesome/pro-duotone-svg-icons';
import type { FC, HTMLAttributes } from 'react';
import { useSip } from '@/providers/sip-provider';
import { useSetAtom } from 'jotai';
import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/sip-state';
import { setOutboundPending } from '@/state/sip-manager';
import { apiClient } from '@/lib/api-client';
import { notify } from '@/lib/toast';
import { cx } from '@/utils/cx';
type PhoneActionCellProps = {
phoneNumber: string;
displayNumber: string;
leadId?: string;
};
```
The component renders:
- The formatted phone number as clickable text (triggers call on click)
- A small kebab menu icon (⋮) on hover that opens a popover with:
- 📞 Call
- 💬 SMS (opens `sms:` link)
- 📱 WhatsApp (opens `https://wa.me/{number}`)
- On mobile: long-press on the phone number opens the same menu
Implementation:
- Use a simple `useState` for menu open/close
- Position the menu absolutely below the phone number
- Click outside closes it
- The Call action uses the same logic as ClickToCallButton (setCallState, setCallerNumber, setOutboundPending, apiClient.post dial)
- SMS opens `sms:+91${phoneNumber}`
- WhatsApp opens `https://wa.me/91${phoneNumber}` in a new tab
- [ ] **Step 2: Handle long-press for mobile**
Add `onContextMenu` (prevents default) and `onTouchStart`/`onTouchEnd` for 500ms long-press detection:
```typescript
const touchTimer = useRef<number | null>(null);
const onTouchStart = () => {
touchTimer.current = window.setTimeout(() => {
setMenuOpen(true);
}, 500);
};
const onTouchEnd = () => {
if (touchTimer.current) {
clearTimeout(touchTimer.current);
touchTimer.current = null;
}
};
```
- [ ] **Step 3: Commit**
```
feat: create PhoneActionCell with call/SMS/WhatsApp context menu
```
---
## Task 3: Redesign the worklist table columns
Replace the current 6-column layout with the new 5-column layout.
**Files:**
- Modify: `helix-engage/src/components/call-desk/worklist-panel.tsx`
- [ ] **Step 1: Import PhoneActionCell**
```typescript
import { PhoneActionCell } from './phone-action-cell';
```
- [ ] **Step 2: Replace table headers**
```typescript
<Table.Header>
<Table.Head label="PRIORITY" className="w-20" isRowHeader />
<Table.Head label="PATIENT" />
<Table.Head label="PHONE" />
<Table.Head label="SOURCE" className="w-28" />
<Table.Head label="SLA" className="w-24" />
</Table.Header>
```
- [ ] **Step 3: Redesign PATIENT cell with sub-line**
```typescript
<Table.Cell>
<div className="flex items-center gap-2">
{row.direction === 'inbound' && (
<IconInbound className="size-3.5 text-fg-success-secondary shrink-0" />
)}
{row.direction === 'outbound' && (
<IconOutbound className="size-3.5 text-fg-brand-secondary shrink-0" />
)}
<div className="min-w-0">
<span className="text-sm font-medium text-primary truncate block max-w-[180px]">
{row.name}
</span>
<span className="text-xs text-tertiary truncate block max-w-[180px]">
{row.lastContactedAt
? `${formatTimeAgo(row.lastContactedAt)}${row.lastDisposition ? `${formatDisposition(row.lastDisposition)}` : ''}`
: row.reason || row.typeLabel}
</span>
</div>
</div>
</Table.Cell>
```
- [ ] **Step 4: Replace PHONE cell with PhoneActionCell**
```typescript
<Table.Cell>
{row.phoneRaw ? (
<PhoneActionCell
phoneNumber={row.phoneRaw}
displayNumber={row.phone}
leadId={row.leadId ?? undefined}
/>
) : (
<span className="text-xs text-quaternary italic">No phone</span>
)}
</Table.Cell>
```
- [ ] **Step 5: Add SOURCE cell**
```typescript
<Table.Cell>
{row.source ? (
<span className="text-xs text-tertiary truncate block max-w-[100px]">
{formatSource(row.source)}
</span>
) : (
<span className="text-xs text-quaternary"></span>
)}
</Table.Cell>
```
- [ ] **Step 6: Update SLA to use lastContacted**
Change `computeSla` to accept a `lastContactedAt` fallback:
```typescript
const sla = computeSla(row.lastContactedAt ?? row.createdAt);
```
- [ ] **Step 7: Remove ACTIONS column and TYPE column**
The TYPE info moves to the tab filter (already there) and the badge on the patient sub-line. The ACTIONS column is replaced by the clickable phone.
- [ ] **Step 8: Add helper functions**
```typescript
const formatTimeAgo = (dateStr: string): string => {
const minutes = Math.round((Date.now() - new Date(dateStr).getTime()) / 60000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
return `${Math.floor(hours / 24)}d ago`;
};
const formatDisposition = (disposition: string): string => {
return disposition.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
};
const formatSource = (source: string): string => {
const map: Record<string, string> = {
FACEBOOK_AD: 'Facebook',
GOOGLE_AD: 'Google',
WALK_IN: 'Walk-in',
REFERRAL: 'Referral',
WEBSITE: 'Website',
PHONE_INQUIRY: 'Phone',
};
return map[source] ?? source.replace(/_/g, ' ');
};
```
- [ ] **Step 9: Remove ClickToCallButton import**
No longer needed in the worklist panel — PhoneActionCell handles it.
- [ ] **Step 10: Commit**
```
feat: redesign worklist table with clickable phones and interaction context
```
---
## Task 4: Add notification badges for new items
When new missed calls or follow-ups arrive (detected via the 30-second refresh), show a visual indicator.
**Files:**
- Modify: `helix-engage/src/components/call-desk/worklist-panel.tsx`
- [ ] **Step 1: Track previous counts to detect new items**
```typescript
const [prevMissedCount, setPrevMissedCount] = useState(missedCount);
useEffect(() => {
if (missedCount > prevMissedCount && prevMissedCount > 0) {
notify.info('New Missed Call', `${missedCount - prevMissedCount} new missed call(s)`);
}
setPrevMissedCount(missedCount);
}, [missedCount, prevMissedCount]);
```
- [ ] **Step 2: Add pulsing dot to tab badges when new items exist**
In the tab items, add a visual indicator for tabs with urgent items:
```typescript
const tabItems = [
{ id: 'all' as const, label: 'All Tasks', badge: allRows.length > 0 ? String(allRows.length) : undefined },
{ id: 'missed' as const, label: 'Missed Calls', badge: missedCount > 0 ? String(missedCount) : undefined, hasNew: missedCount > prevMissedCount },
// ...
];
```
The Tab component already supports badges. For the "new" indicator, append a small red dot after the badge number using a custom render if needed.
- [ ] **Step 3: Commit**
```
feat: add notification for new missed calls in worklist
```
---
## Task 5: Deploy and verify
- [ ] **Step 1: Type check**
```bash
cd helix-engage && npx tsc --noEmit
```
- [ ] **Step 2: Build and deploy**
```bash
VITE_API_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
```
- [ ] **Step 3: Test clickable phone**
1. Hover over a phone number — kebab menu icon appears
2. Click phone number directly — places outbound call
3. Click kebab → SMS — opens SMS app
4. Click kebab → WhatsApp — opens WhatsApp web
5. On mobile: long-press phone number — context menu appears
- [ ] **Step 4: Test last interaction context**
1. Leads with `lastContacted` show "2h ago — Info Provided" sub-line
2. Leads without `lastContacted` show interested service or type
3. Missed calls show "Missed at 2:30 PM"
- [ ] **Step 5: Test SLA**
1. SLA shows time since last contact (not creation)
2. Green < 15m, amber 15-30m, red > 30m
---
## Notes
- **No schema changes needed** — all data is already available from the platform
- **ClickToCallButton stays** — it's still used in the active call card for the ringing-out End Call button. Only the worklist replaces it with PhoneActionCell.
- **WhatsApp link format** — `https://wa.me/91XXXXXXXXXX` (no + prefix, includes country code)
- **SMS link format** — `sms:+91XXXXXXXXXX` (with + prefix)
- **The TYPE column is removed** — the tab filter already categorizes by type, and the patient sub-line shows context. Adding a TYPE badge to each row is redundant.
- **Filter out no-phone follow-ups** — optional future improvement. For now, show "No phone" in italic which makes it clear the agent can't call.