mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: add Follow-ups and Call History pages for CC Agent
Implements full Follow-ups page with overdue/upcoming/completed sections and left-border color-coded cards. Adds Call History table filtered by current agent with disposition badges and duration formatting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,125 @@
|
|||||||
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { TopBar } from '@/components/layout/top-bar';
|
import { TopBar } from '@/components/layout/top-bar';
|
||||||
|
import { formatShortDate } from '@/lib/format';
|
||||||
|
import { useData } from '@/providers/data-provider';
|
||||||
|
import { useAuth } from '@/providers/auth-provider';
|
||||||
|
import type { CallDisposition } from '@/types/entities';
|
||||||
|
|
||||||
|
const dispositionColor = (disposition: CallDisposition | null): 'success' | 'brand' | 'blue-light' | 'warning' | 'gray' | 'error' => {
|
||||||
|
switch (disposition) {
|
||||||
|
case 'APPOINTMENT_BOOKED':
|
||||||
|
return 'success';
|
||||||
|
case 'FOLLOW_UP_SCHEDULED':
|
||||||
|
return 'brand';
|
||||||
|
case 'INFO_PROVIDED':
|
||||||
|
return 'blue-light';
|
||||||
|
case 'NO_ANSWER':
|
||||||
|
return 'warning';
|
||||||
|
case 'WRONG_NUMBER':
|
||||||
|
return 'gray';
|
||||||
|
case 'CALLBACK_REQUESTED':
|
||||||
|
return 'brand';
|
||||||
|
default:
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDispositionLabel = (disposition: CallDisposition | null): string => {
|
||||||
|
if (!disposition) return '—';
|
||||||
|
return disposition
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDuration = (seconds: number | null): string => {
|
||||||
|
if (seconds === null) return '—';
|
||||||
|
const mins = Math.round(seconds / 60);
|
||||||
|
return mins === 0 ? '<1 min' : `${mins} min`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCallerNumber = (callerNumber: { number: string; callingCode: string }[] | null): string => {
|
||||||
|
if (!callerNumber || callerNumber.length === 0) return '—';
|
||||||
|
const first = callerNumber[0];
|
||||||
|
return `${first.callingCode} ${first.number}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const CallHistoryPage = () => {
|
export const CallHistoryPage = () => {
|
||||||
|
const { calls } = useData();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const agentCalls = calls
|
||||||
|
.filter((call) => call.agentName === user.name)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const dateA = a.startedAt ? new Date(a.startedAt).getTime() : 0;
|
||||||
|
const dateB = b.startedAt ? new Date(b.startedAt).getTime() : 0;
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<TopBar title="Call History" subtitle="Past call logs and recordings" />
|
<TopBar title="Call History" subtitle="All inbound calls" />
|
||||||
<div className="flex flex-1 items-center justify-center p-7">
|
<div className="flex-1 overflow-y-auto p-7">
|
||||||
|
{agentCalls.length === 0 ? (
|
||||||
|
<div className="flex flex-1 items-center justify-center py-20">
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
<div className="flex flex-col items-center gap-2 text-center">
|
||||||
<h2 className="text-display-xs font-bold text-primary">Call History</h2>
|
<h3 className="text-sm font-semibold text-primary">No calls found</h3>
|
||||||
<p className="text-sm text-tertiary">Coming soon — call logs, recordings, and outcome tracking.</p>
|
<p className="text-sm text-tertiary">No call history available for your account yet.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-2xl border border-secondary bg-primary overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-secondary">
|
||||||
|
<th className="text-xs uppercase tracking-wider text-quaternary font-semibold px-4 py-3 text-left">
|
||||||
|
Date / Time
|
||||||
|
</th>
|
||||||
|
<th className="text-xs uppercase tracking-wider text-quaternary font-semibold px-4 py-3 text-left">
|
||||||
|
Caller
|
||||||
|
</th>
|
||||||
|
<th className="text-xs uppercase tracking-wider text-quaternary font-semibold px-4 py-3 text-left">
|
||||||
|
Lead Name
|
||||||
|
</th>
|
||||||
|
<th className="text-xs uppercase tracking-wider text-quaternary font-semibold px-4 py-3 text-left">
|
||||||
|
Duration
|
||||||
|
</th>
|
||||||
|
<th className="text-xs uppercase tracking-wider text-quaternary font-semibold px-4 py-3 text-left">
|
||||||
|
Disposition
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{agentCalls.map((call) => (
|
||||||
|
<tr key={call.id} className="border-b border-tertiary hover:bg-primary_hover transition duration-100 ease-linear">
|
||||||
|
<td className="px-4 py-3 text-sm text-secondary whitespace-nowrap">
|
||||||
|
{call.startedAt ? formatShortDate(call.startedAt) : '—'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-secondary whitespace-nowrap">
|
||||||
|
{formatCallerNumber(call.callerNumber)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-primary">
|
||||||
|
{call.leadName ?? '—'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-secondary whitespace-nowrap">
|
||||||
|
{formatDuration(call.durationSeconds)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
{call.disposition ? (
|
||||||
|
<Badge size="sm" color={dispositionColor(call.disposition)}>
|
||||||
|
{formatDispositionLabel(call.disposition)}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-tertiary">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,151 @@
|
|||||||
|
import { Badge } from '@/components/base/badges/badges';
|
||||||
import { TopBar } from '@/components/layout/top-bar';
|
import { TopBar } from '@/components/layout/top-bar';
|
||||||
|
import { formatShortDate } from '@/lib/format';
|
||||||
|
import { useFollowUps } from '@/hooks/use-follow-ups';
|
||||||
|
import { cx } from '@/utils/cx';
|
||||||
|
import type { FollowUp, FollowUpStatus, Priority } from '@/types/entities';
|
||||||
|
|
||||||
export const FollowUpsPage = () => {
|
const statusColor = (status: FollowUpStatus | null): 'error' | 'brand' | 'success' | 'gray' => {
|
||||||
|
switch (status) {
|
||||||
|
case 'OVERDUE':
|
||||||
|
return 'error';
|
||||||
|
case 'PENDING':
|
||||||
|
return 'brand';
|
||||||
|
case 'COMPLETED':
|
||||||
|
return 'success';
|
||||||
|
case 'CANCELLED':
|
||||||
|
default:
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityColor = (priority: Priority | null): 'error' | 'warning' | 'brand' | 'gray' => {
|
||||||
|
switch (priority) {
|
||||||
|
case 'URGENT':
|
||||||
|
return 'error';
|
||||||
|
case 'HIGH':
|
||||||
|
return 'warning';
|
||||||
|
case 'NORMAL':
|
||||||
|
return 'brand';
|
||||||
|
case 'LOW':
|
||||||
|
default:
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardBorderClass = (status: FollowUpStatus | null, isOverdue: boolean): string => {
|
||||||
|
if (isOverdue || status === 'OVERDUE') {
|
||||||
|
return 'border-l-error-500 bg-error-primary';
|
||||||
|
}
|
||||||
|
if (status === 'COMPLETED') {
|
||||||
|
return 'border-l-success-500';
|
||||||
|
}
|
||||||
|
if (status === 'CANCELLED') {
|
||||||
|
return 'border-l-gray-300';
|
||||||
|
}
|
||||||
|
return 'border-l-brand-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FollowUpCardProps {
|
||||||
|
followUp: FollowUp;
|
||||||
|
isOverdue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FollowUpCard = ({ followUp, isOverdue = false }: FollowUpCardProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col">
|
<div className={cx('rounded-xl border-l-[3px] bg-primary p-4', cardBorderClass(followUp.followUpStatus, isOverdue))}>
|
||||||
<TopBar title="Follow-ups" subtitle="Scheduled follow-up tasks" />
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex flex-1 items-center justify-center p-7">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
<p
|
||||||
<h2 className="text-display-xs font-bold text-primary">Follow-ups</h2>
|
className={cx(
|
||||||
<p className="text-sm text-tertiary">Coming soon — follow-up reminders, scheduling, and task management.</p>
|
'text-xs font-semibold',
|
||||||
|
isOverdue || followUp.followUpStatus === 'OVERDUE' ? 'text-error-primary' : 'text-brand-secondary',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{followUp.scheduledAt ? formatShortDate(followUp.scheduledAt) : 'No date scheduled'}
|
||||||
|
{(isOverdue || followUp.followUpStatus === 'OVERDUE') && ' — Overdue'}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm font-medium text-primary">
|
||||||
|
{followUp.description ?? followUp.followUpType ?? 'Follow-up'}
|
||||||
|
</p>
|
||||||
|
{(followUp.patientName || followUp.patientPhone) && (
|
||||||
|
<p className="mt-0.5 text-xs text-tertiary">
|
||||||
|
{followUp.patientName}
|
||||||
|
{followUp.patientPhone && ` · ${followUp.patientPhone}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 flex-col items-end gap-1.5">
|
||||||
|
{followUp.followUpStatus && (
|
||||||
|
<Badge size="sm" color={statusColor(followUp.followUpStatus)}>
|
||||||
|
{followUp.followUpStatus}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{followUp.priority && (
|
||||||
|
<Badge size="sm" color={priorityColor(followUp.priority)}>
|
||||||
|
{followUp.priority}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FollowUpsPage = () => {
|
||||||
|
const { followUps, overdue, upcoming } = useFollowUps();
|
||||||
|
|
||||||
|
const completed = followUps.filter((f) => f.followUpStatus === 'COMPLETED');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<TopBar title="Follow-ups" subtitle="Scheduled callbacks and reminders" />
|
||||||
|
<div className="flex-1 overflow-y-auto p-7 space-y-6">
|
||||||
|
{/* Overdue section */}
|
||||||
|
{overdue.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-sm font-bold text-error-primary">Overdue ({overdue.length})</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{overdue.map((followUp) => (
|
||||||
|
<FollowUpCard key={followUp.id} followUp={followUp} isOverdue />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upcoming section */}
|
||||||
|
{upcoming.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-sm font-bold text-primary">Upcoming ({upcoming.length})</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{upcoming.map((followUp) => (
|
||||||
|
<FollowUpCard key={followUp.id} followUp={followUp} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Completed section */}
|
||||||
|
{completed.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="mb-3 text-sm font-bold text-tertiary">Completed ({completed.length})</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{completed.map((followUp) => (
|
||||||
|
<FollowUpCard key={followUp.id} followUp={followUp} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{overdue.length === 0 && upcoming.length === 0 && completed.length === 0 && (
|
||||||
|
<div className="flex flex-1 items-center justify-center py-20">
|
||||||
|
<div className="flex flex-col items-center gap-2 text-center">
|
||||||
|
<h3 className="text-sm font-semibold text-primary">No follow-ups</h3>
|
||||||
|
<p className="text-sm text-tertiary">All caught up — no scheduled callbacks or reminders.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user