mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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:
435
docs/superpowers/plans/2026-03-20-worklist-ux-redesign.md
Normal file
435
docs/superpowers/plans/2026-03-20-worklist-ux-redesign.md
Normal 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.
|
||||
Reference in New Issue
Block a user