mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
QA flagged Team Dashboard vs Team Performance as repetitive. Retire Team Performance from the sidebar; move its unique surfaces (rich agent table, time breakdown, NPS/Conversion, Performance Alerts) into Team Dashboard below the existing KPI row. - supervisor-rollup: new shared module — useSupervisorRollup hook + RichAgentTable / TimeBreakdown / NpsConversion / PerformanceAlerts - Time Breakdown rendered as a table (Agent / Active / Wrap / Idle / Break / Total + Team-average header row) — QA flagged the old stacked-bar tiles as misleading because per-agent totals varied wildly and width comparison was meaningless - team-dashboard: tabs replaced with stacked sections; everything scroll-visible so supervisors don't hunt across surfaces - sidebar: remove 'Team Performance' entry (route kept for backup) and drop the now-unused IconChartLine wiring
172 lines
8.9 KiB
TypeScript
172 lines
8.9 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import { faSidebarFlip, faSidebar } from '@fortawesome/pro-duotone-svg-icons';
|
|
import { AiChatPanel } from '@/components/call-desk/ai-chat-panel';
|
|
import { DashboardKpi } from '@/components/dashboard/kpi-cards';
|
|
import { MissedQueue } from '@/components/dashboard/missed-queue';
|
|
import {
|
|
RichAgentTable,
|
|
TimeBreakdown,
|
|
NpsConversion,
|
|
PerformanceAlerts,
|
|
useSupervisorRollup,
|
|
} from '@/components/dashboard/supervisor-rollup';
|
|
import { useData } from '@/providers/data-provider';
|
|
import { cx } from '@/utils/cx';
|
|
|
|
type DateRange = 'today' | 'week' | 'month';
|
|
|
|
const getDateRangeStart = (range: DateRange): Date => {
|
|
const now = new Date();
|
|
switch (range) {
|
|
case 'today': { const s = new Date(now); s.setHours(0, 0, 0, 0); return s; }
|
|
case 'week': { const s = new Date(now); s.setDate(s.getDate() - 7); return s; }
|
|
case 'month': { const s = new Date(now); s.setDate(s.getDate() - 30); return s; }
|
|
}
|
|
};
|
|
|
|
export const TeamDashboardPage = () => {
|
|
const { calls, leads, campaigns, loading } = useData();
|
|
const [dateRange, setDateRange] = useState<DateRange>('week');
|
|
const [aiOpen, setAiOpen] = useState(true);
|
|
|
|
// Pull the richer supervisor rollup (NPS, idle, time breakdown, alerts)
|
|
// from the sidecar. Only `today`/`week`/`month` overlap with the rollup's
|
|
// date-range semantics — map them through directly.
|
|
const { agents: rollupAgents } = useSupervisorRollup(dateRange);
|
|
|
|
const filteredCalls = useMemo(() => {
|
|
const rangeStart = getDateRangeStart(dateRange);
|
|
return calls.filter((call) => {
|
|
if (!call.startedAt) return false;
|
|
return new Date(call.startedAt) >= rangeStart;
|
|
});
|
|
}, [calls, dateRange]);
|
|
|
|
const dateRangeLabel = dateRange === 'today' ? 'Today' : dateRange === 'week' ? 'This Week' : 'This Month';
|
|
|
|
const convRate = useMemo(() => {
|
|
if (filteredCalls.length === 0) return 0;
|
|
const completed = filteredCalls.filter((c) => c.callStatus === 'COMPLETED').length;
|
|
return Math.round((completed / filteredCalls.length) * 100);
|
|
}, [filteredCalls]);
|
|
|
|
const missedQueueCount = filteredCalls.filter((c) => c.callStatus === 'MISSED').length;
|
|
|
|
return (
|
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex shrink-0 items-center justify-between border-b border-secondary px-6 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-lg font-bold text-primary">Team Dashboard</h1>
|
|
<span className="text-sm text-tertiary">{dateRangeLabel}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex rounded-lg border border-secondary overflow-hidden">
|
|
{(['today', 'week', 'month'] as const).map((range) => (
|
|
<button
|
|
key={range}
|
|
onClick={() => setDateRange(range)}
|
|
className={cx(
|
|
"px-3 py-1 text-xs font-medium transition duration-100 ease-linear",
|
|
dateRange === range ? 'bg-active text-brand-secondary' : 'text-tertiary hover:bg-primary_hover',
|
|
)}
|
|
>
|
|
{range === 'today' ? 'Today' : range === 'week' ? 'Week' : 'Month'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
onClick={() => setAiOpen(!aiOpen)}
|
|
className="flex size-8 items-center justify-center rounded-lg text-fg-quaternary hover:text-fg-secondary hover:bg-primary_hover transition duration-100 ease-linear"
|
|
title={aiOpen ? 'Hide AI panel' : 'Show AI panel'}
|
|
>
|
|
<FontAwesomeIcon icon={aiOpen ? faSidebarFlip : faSidebar} className="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Main content — scrollable column with KPIs pinned at the
|
|
top, then stacked supervisor sections (Agent table, Time
|
|
breakdown, NPS/Conv, Alerts, Missed Queue, Campaigns).
|
|
No tabs: everything is scroll-visible so a supervisor
|
|
doesn't have to hunt across surfaces for their metrics. */}
|
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
|
<div className="px-6 pt-5 pb-3">
|
|
<DashboardKpi calls={filteredCalls} leads={leads} />
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-5 px-6 pb-8">
|
|
{loading && rollupAgents.length === 0 ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<p className="text-sm text-tertiary">Loading...</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<RichAgentTable agents={rollupAgents} />
|
|
|
|
<TimeBreakdown agents={rollupAgents} />
|
|
|
|
<NpsConversion agents={rollupAgents} convRate={convRate} />
|
|
|
|
<PerformanceAlerts agents={rollupAgents} />
|
|
|
|
<div className="rounded-xl border border-secondary bg-primary p-4">
|
|
<h3 className="text-sm font-semibold text-secondary mb-3">
|
|
Missed Queue ({missedQueueCount})
|
|
</h3>
|
|
<MissedQueue calls={filteredCalls} />
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-secondary bg-primary p-4">
|
|
<h3 className="text-sm font-semibold text-secondary mb-3">
|
|
Campaigns ({campaigns.length})
|
|
</h3>
|
|
{campaigns.length === 0 ? (
|
|
<p className="text-sm text-tertiary py-4 text-center">No campaigns</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{campaigns.map((c) => (
|
|
<div key={c.id} className="flex items-center justify-between rounded-lg border border-secondary bg-primary p-4">
|
|
<div>
|
|
<span className="text-sm font-semibold text-primary">{c.campaignName}</span>
|
|
<div className="flex items-center gap-3 mt-1 text-xs text-tertiary">
|
|
<span>{c.campaignStatus}</span>
|
|
<span>{c.platform}</span>
|
|
<span>{c.leadCount} leads</span>
|
|
<span>{c.convertedCount} converted</span>
|
|
</div>
|
|
</div>
|
|
{c.budget && (
|
|
<span className="text-sm font-medium text-secondary">
|
|
₹{Math.round(c.budget.amountMicros / 1_000_000).toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI panel — collapsible */}
|
|
<div className={cx(
|
|
"shrink-0 border-l border-secondary bg-primary flex flex-col overflow-hidden transition-all duration-200 ease-linear",
|
|
aiOpen ? "w-[380px]" : "w-0 border-l-0",
|
|
)}>
|
|
{aiOpen && (
|
|
<div className="flex h-full flex-col p-4">
|
|
<AiChatPanel callerContext={{ type: 'supervisor' }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
};
|