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>
14 KiB
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
- Phone column is passive text — separate Call button in Actions column wastes space
- No last interaction context — agent doesn't know what happened before
- No campaign/source — agent can't personalize the opening
- SLA shows time since creation, not time since last contact
- Rows without phone numbers are dead weight
- 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(notcreatedAt). Falls back tocreatedAtif 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:
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
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:
rows.push({
// ... existing ...
lastContactedAt: lead.lastContacted ?? null,
contactAttempts: lead.contactAttempts ?? 0,
source: lead.leadSource ?? lead.utmCampaign ?? null,
lastDisposition: null,
});
For missed calls:
rows.push({
// ... existing ...
lastContactedAt: call.startedAt ?? call.createdAt,
contactAttempts: 0,
source: null,
lastDisposition: call.disposition ?? null,
});
For follow-ups:
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:
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
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
useStatefor 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:
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
import { PhoneActionCell } from './phone-action-cell';
- Step 2: Replace table headers
<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
<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
<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
<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:
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
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
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:
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
cd helix-engage && npx tsc --noEmit
- Step 2: Build and deploy
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
- Hover over a phone number — kebab menu icon appears
- Click phone number directly — places outbound call
- Click kebab → SMS — opens SMS app
- Click kebab → WhatsApp — opens WhatsApp web
- On mobile: long-press phone number — context menu appears
- Step 4: Test last interaction context
- Leads with
lastContactedshow "2h ago — Info Provided" sub-line - Leads without
lastContactedshow interested service or type - Missed calls show "Missed at 2:30 PM"
- Step 5: Test SLA
- SLA shows time since last contact (not creation)
- 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.