mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
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>
436 lines
14 KiB
Markdown
436 lines
14 KiB
Markdown
# 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.
|