Files
helix-engage/docs/superpowers/plans/2026-03-20-worklist-ux-redesign.md
saridsa2 3064eeb444 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>
2026-03-21 10:36:10 +05:30

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

  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:

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 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:

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
  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 formathttps://wa.me/91XXXXXXXXXX (no + prefix, includes country code)
  • SMS link formatsms:+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.