Files
helix-engage/src/pages/team-dashboard.tsx
saridsa2 4598740efe feat: inline forms, transfer redesign, patient fixes, UI polish
- Appointment/enquiry forms reverted to inline rendering (not modals)
- Forms: flat scrollable section with pinned footer, no card wrapper
- Appointment form: DatePicker component, date prefilled, removed Returning Patient checkbox
- Enquiry form: removed disposition dropdown, lead status defaults to CONTACTED
- Transfer dialog: agent picker with live status, doctor list with department, select-then-connect flow
- Transfer: removed external number input, moved Cancel/Connect to pinned header row
- Button mutual exclusivity: Book Appt / Enquiry / Transfer close each other
- Patient name write-back: appointment + enquiry forms update patient fullName after save
- Caller cache invalidation: POST /api/caller/invalidate after name update
- Follow-up fix (#513): assignedAgent, patientId, date validation in createFollowUp
- Patients page: removed status filters + column, added pagination (15/page)
- Pending badge removed from call desk header
- Table resize handles visible (bg-tertiary pill)
- Sim call button: dev-only (import.meta.env.DEV)
- CallControlStrip component (reusable, not currently mounted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 12:14:38 +05:30

165 lines
8.2 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 { AgentTable } from '@/components/dashboard/agent-table';
import { MissedQueue } from '@/components/dashboard/missed-queue';
import { useData } from '@/providers/data-provider';
import { cx } from '@/utils/cx';
type DateRange = 'today' | 'week' | 'month';
type DashboardTab = 'agents' | 'missed' | 'campaigns';
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 [tab, setTab] = useState<DashboardTab>('agents');
const [aiOpen, setAiOpen] = useState(true);
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 tabs = [
{ id: 'agents' as const, label: 'Agent Performance' },
{ id: 'missed' as const, label: `Missed Queue (${filteredCalls.filter(c => c.callStatus === 'MISSED').length})` },
{ id: 'campaigns' as const, label: `Campaigns (${campaigns.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 */}
<div className="flex flex-1 flex-col overflow-y-auto">
{/* KPI cards — always visible */}
<div className="px-6 pt-5 pb-3">
<DashboardKpi calls={filteredCalls} leads={leads} />
</div>
{/* Tabs */}
<div className="flex items-center gap-1 border-b border-secondary px-6">
{tabs.map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={cx(
"px-3 py-2.5 text-xs font-semibold transition duration-100 ease-linear border-b-2",
tab === t.id
? "border-brand text-brand-secondary"
: "border-transparent text-tertiary hover:text-secondary",
)}
>
{t.label}
</button>
))}
</div>
{/* Tab content */}
<div className="flex-1 p-6">
{loading && (
<div className="flex items-center justify-center py-12">
<p className="text-sm text-tertiary">Loading...</p>
</div>
)}
{!loading && tab === 'agents' && (
<AgentTable calls={filteredCalls} />
)}
{!loading && tab === 'missed' && (
<MissedQueue calls={filteredCalls} />
)}
{!loading && tab === 'campaigns' && (
<div className="space-y-3">
{campaigns.length === 0 ? (
<p className="text-sm text-tertiary py-12 text-center">No campaigns</p>
) : (
campaigns.map((c) => (
<div key={c.id} className="flex items-center justify-between rounded-xl border border-secondary bg-primary p-4 shadow-xs">
<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>
{/* 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 />
</div>
)}
</div>
</div>
</div>
);
};