@@ -111,112 +251,124 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
}
return (
-
- {/* Missed calls */}
- {missedCalls.length > 0 && (
-
-
-
- {missedCalls.map((call) => {
- const phone = call.callerNumber?.[0];
- const phoneDisplay = phone ? formatPhone(phone) : 'Unknown number';
- const phoneNumber = phone?.number ?? '';
- return (
-
-
-
- {phoneDisplay}
-
- {call.createdAt ? formatAge(call.createdAt) : 'Unknown'}
-
-
- {call.startedAt && (
-
- {new Date(call.startedAt).toLocaleTimeString('en-IN', { hour: 'numeric', minute: '2-digit', hour12: true })}
-
- )}
-
-
-
- );
- })}
+
+
+
+ setSearch(value)}
+ aria-label="Search worklist"
+ />
+
-
- )}
+ }
+ />
- {/* Follow-ups */}
- {followUps.length > 0 && (
-
-
-
- {followUps.map((fu) => {
- const isOverdue = fu.followUpStatus === 'OVERDUE' ||
- (fu.scheduledAt && new Date(fu.scheduledAt) < new Date());
- const label = followUpLabel[fu.followUpType ?? ''] ?? fu.followUpType ?? 'Follow-up';
- const priority = priorityConfig[fu.priority ?? 'NORMAL'] ?? priorityConfig.NORMAL;
+ {/* Filter tabs */}
+
+ setTab(key as TabKey)}>
+
+ {(item) => }
+
+
+
+
+ {filteredRows.length === 0 ? (
+
+
+ {search ? 'No matching items' : 'No items in this category'}
+
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+ {(row) => {
+ const priority = priorityConfig[row.priority] ?? priorityConfig.NORMAL;
+ const sla = computeSla(row.createdAt);
+ const typeCfg = typeConfig[row.type];
+ const isSelected = row.originalLead !== null && row.originalLead.id === selectedLeadId;
return (
-
-
- {label}
- {isOverdue && Overdue }
- {priority.label}
-
- {fu.scheduledAt && (
-
- {new Date(fu.scheduledAt).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })}
-
- )}
-
- );
- })}
-
-
- )}
-
- {/* Assigned leads */}
- {leads.length > 0 && (
-
-
-
- {leads.map((lead) => {
- const firstName = lead.contactName?.firstName ?? '';
- const lastName = lead.contactName?.lastName ?? '';
- const fullName = `${firstName} ${lastName}`.trim() || 'Unknown';
- const phone = lead.contactPhone?.[0];
- const phoneDisplay = phone ? formatPhone(phone) : '';
- const phoneNumber = phone?.number ?? '';
- const isSelected = lead.id === selectedLeadId;
-
- return (
-
onSelectLead(lead)}
+
{
+ if (row.originalLead) {
+ onSelectLead(row.originalLead);
+ }
+ }}
>
-
-
-
{fullName}
- {phoneDisplay &&
{phoneDisplay} }
+
+
+ {priority.label}
+
+
+
+
+ {row.direction === 'inbound' && (
+
+ )}
+ {row.direction === 'outbound' && (
+
+ )}
+
+ {row.name}
+
- {lead.interestedService && (
- {lead.interestedService}
- )}
-
-
-
+
+
+
+ {row.phone || '\u2014'}
+
+
+
+
+ {row.typeLabel}
+
+
+
+
+ {sla.label}
+
+
+
+
+ {row.phoneRaw ? (
+
+ ) : (
+ No phone
+ )}
+
+
+
);
- })}
-
-
+ }}
+
+
)}
-
+
);
};
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx
index 85962e7..bdc64a6 100644
--- a/src/components/layout/sidebar.tsx
+++ b/src/components/layout/sidebar.tsx
@@ -10,6 +10,7 @@ import {
faCommentDots,
faGear,
faGrid2,
+ faHospitalUser,
faPhone,
faPlug,
faUsers,
@@ -58,6 +59,9 @@ const IconClockRewind: FC
> = ({ className }) =>
const IconUsers: FC> = ({ className }) => (
);
+const IconHospitalUser: FC> = ({ className }) => (
+
+);
type NavSection = {
label: string;
@@ -70,7 +74,7 @@ const getNavSections = (role: string): NavSection[] => {
{ label: 'Overview', items: [{ label: 'Team Dashboard', href: '/', icon: IconGrid2 }] },
{ label: 'Management', items: [
{ label: 'Campaigns', href: '/campaigns', icon: IconBullhorn },
- { label: 'Analytics', href: '/analytics', icon: IconChartMixed },
+ { label: 'Analytics', href: '/reports', icon: IconChartMixed },
]},
{ label: 'Admin', items: [
{ label: 'Integrations', href: '/integrations', icon: IconPlug },
@@ -83,6 +87,7 @@ const getNavSections = (role: string): NavSection[] => {
return [
{ label: 'Call Center', items: [
{ label: 'Call Desk', href: '/', icon: IconPhone },
+ { label: 'Patients', href: '/patients', icon: IconHospitalUser },
{ label: 'Follow-ups', href: '/follow-ups', icon: IconBell },
{ label: 'Call History', href: '/call-history', icon: IconClockRewind },
]},
@@ -93,11 +98,12 @@ const getNavSections = (role: string): NavSection[] => {
{ label: 'Main', items: [
{ label: 'Lead Workspace', href: '/', icon: IconGrid2 },
{ label: 'All Leads', href: '/leads', icon: IconUsers },
+ { label: 'Patients', href: '/patients', icon: IconHospitalUser },
{ label: 'Campaigns', href: '/campaigns', icon: IconBullhorn },
{ label: 'Outreach', href: '/outreach', icon: IconCommentDots },
]},
{ label: 'Insights', items: [
- { label: 'Analytics', href: '/analytics', icon: IconChartMixed },
+ { label: 'Analytics', href: '/reports', icon: IconChartMixed },
]},
];
};
diff --git a/src/lib/transforms.ts b/src/lib/transforms.ts
index 81d27d1..1263c45 100644
--- a/src/lib/transforms.ts
+++ b/src/lib/transforms.ts
@@ -1,7 +1,7 @@
// Transform platform GraphQL responses → frontend entity types
// Platform remaps some field names during sync
-import type { Lead, Campaign, Ad, FollowUp, LeadActivity, Call } from '@/types/entities';
+import type { Lead, Campaign, Ad, FollowUp, LeadActivity, Call, Patient } from '@/types/entities';
type PlatformNode = Record;
@@ -150,3 +150,16 @@ export function transformCalls(data: any): Call[] {
leadId: n.leadId,
}));
}
+
+export function transformPatients(data: any): Patient[] {
+ return extractEdges(data, 'patients').map((n) => ({
+ id: n.id,
+ createdAt: n.createdAt,
+ fullName: n.fullName ?? null,
+ phones: n.phones ?? null,
+ emails: n.emails ?? null,
+ dateOfBirth: n.dateOfBirth ?? null,
+ gender: n.gender ?? null,
+ patientType: n.patientType ?? null,
+ }));
+}
diff --git a/src/main.tsx b/src/main.tsx
index 9ecb80e..3843ee9 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -14,6 +14,8 @@ import { FollowUpsPage } from "@/pages/follow-ups-page";
import { LoginPage } from "@/pages/login";
import { OutreachPage } from "@/pages/outreach";
import { Patient360Page } from "@/pages/patient-360";
+import { ReportsPage } from "@/pages/reports";
+import { PatientsPage } from "@/pages/patients";
import { TeamDashboardPage } from "@/pages/team-dashboard";
import { AuthProvider } from "@/providers/auth-provider";
import { DataProvider } from "@/providers/data-provider";
@@ -47,7 +49,9 @@ createRoot(document.getElementById("root")!).render(
} />
} />
} />
+ } />
} />
+ } />
} />
} />
diff --git a/src/pages/call-desk.tsx b/src/pages/call-desk.tsx
index 24f7df1..d637a9f 100644
--- a/src/pages/call-desk.tsx
+++ b/src/pages/call-desk.tsx
@@ -75,16 +75,14 @@ export const CallDeskPage = () => {
{/* Worklist (visible when idle) */}
{!isInCall && (
-
- setSelectedLead(lead)}
- selectedLeadId={selectedLead?.id ?? null}
- />
-
+ setSelectedLead(lead)}
+ selectedLeadId={selectedLead?.id ?? null}
+ />
)}
{/* Today's calls — always visible */}
diff --git a/src/pages/call-history.tsx b/src/pages/call-history.tsx
index a9ae23f..118ef91 100644
--- a/src/pages/call-history.tsx
+++ b/src/pages/call-history.tsx
@@ -1,124 +1,292 @@
+import { useMemo, useRef, useState } from 'react';
+import type { FC } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faPhoneArrowDown,
+ faPhoneArrowUp,
+ faPhoneXmark,
+ faPlay,
+ faPause,
+} from '@fortawesome/pro-duotone-svg-icons';
+import { SearchLg } from '@untitledui/icons';
+import { Table, TableCard } from '@/components/application/table/table';
import { Badge } from '@/components/base/badges/badges';
+import { Button } from '@/components/base/buttons/button';
+import { Input } from '@/components/base/input/input';
+import { Select } from '@/components/base/select/select';
import { TopBar } from '@/components/layout/top-bar';
-import { formatShortDate } from '@/lib/format';
+import { ClickToCallButton } from '@/components/call-desk/click-to-call-button';
+import { formatShortDate, formatPhone } from '@/lib/format';
import { useData } from '@/providers/data-provider';
-import { useAuth } from '@/providers/auth-provider';
-import type { CallDisposition } from '@/types/entities';
+import type { Call, CallDirection, 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';
- }
-};
+type FilterKey = 'all' | 'inbound' | 'outbound' | 'missed';
-const formatDispositionLabel = (disposition: CallDisposition | null): string => {
- if (!disposition) return '—';
- return disposition
- .toLowerCase()
- .replace(/_/g, ' ')
- .replace(/\b\w/g, (c) => c.toUpperCase());
+const filterItems = [
+ { id: 'all' as const, label: 'All Calls' },
+ { id: 'inbound' as const, label: 'Inbound' },
+ { id: 'outbound' as const, label: 'Outbound' },
+ { id: 'missed' as const, label: 'Missed' },
+];
+
+const dispositionConfig: Record = {
+ APPOINTMENT_BOOKED: { label: 'Appt Booked', color: 'success' },
+ FOLLOW_UP_SCHEDULED: { label: 'Follow-up', color: 'brand' },
+ INFO_PROVIDED: { label: 'Info Provided', color: 'blue-light' },
+ NO_ANSWER: { label: 'No Answer', color: 'warning' },
+ WRONG_NUMBER: { label: 'Wrong Number', color: 'gray' },
+ CALLBACK_REQUESTED: { label: 'Callback', color: 'brand' },
};
const formatDuration = (seconds: number | null): string => {
- if (seconds === null) return '—';
- const mins = Math.round(seconds / 60);
- return mins === 0 ? '<1 min' : `${mins} min`;
+ if (seconds === null || seconds === 0) return '\u2014';
+ if (seconds < 60) return `${seconds}s`;
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
};
-const formatCallerNumber = (callerNumber: { number: string; callingCode: string }[] | null): string => {
- if (!callerNumber || callerNumber.length === 0) return '—';
- const first = callerNumber[0];
- return `${first.callingCode} ${first.number}`;
+const formatPhoneDisplay = (call: Call): string => {
+ if (call.callerNumber && call.callerNumber.length > 0) {
+ return formatPhone(call.callerNumber[0]);
+ }
+ return '\u2014';
+};
+
+const DirectionIcon: FC<{ direction: CallDirection | null; status: Call['callStatus'] }> = ({ direction, status }) => {
+ if (status === 'MISSED') {
+ return ;
+ }
+ if (direction === 'OUTBOUND') {
+ return ;
+ }
+ return ;
+};
+
+const RecordingPlayer: FC<{ url: string }> = ({ url }) => {
+ const audioRef = useRef(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+
+ const togglePlay = () => {
+ const audio = audioRef.current;
+ if (!audio) return;
+ if (isPlaying) {
+ audio.pause();
+ setIsPlaying(false);
+ } else {
+ audio.play().catch(() => setIsPlaying(false));
+ setIsPlaying(true);
+ }
+ };
+
+ const handleEnded = () => setIsPlaying(false);
+
+ return (
+ <>
+
+
+ }
+ onClick={togglePlay}
+ aria-label={isPlaying ? 'Pause recording' : 'Play recording'}
+ />
+ >
+ );
};
export const CallHistoryPage = () => {
- const { calls } = useData();
- const { user } = useAuth();
+ const { calls, leads } = useData();
+ const [search, setSearch] = useState('');
+ const [filter, setFilter] = useState('all');
- const agentCalls = calls
- .filter((call) => call.agentName === user.name)
- .sort((a, b) => {
+ // Build a map of lead names by ID for enrichment
+ const leadNameMap = useMemo(() => {
+ const map = new Map();
+ for (const lead of leads) {
+ if (lead.id && lead.contactName) {
+ const name = `${lead.contactName.firstName ?? ''} ${lead.contactName.lastName ?? ''}`.trim();
+ if (name) map.set(lead.id, name);
+ }
+ }
+ return map;
+ }, [leads]);
+
+ // Sort by time (newest first) and apply filters
+ const filteredCalls = useMemo(() => {
+ let result = [...calls].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;
});
+ // Direction / status filter
+ if (filter === 'inbound') result = result.filter((c) => c.callDirection === 'INBOUND');
+ else if (filter === 'outbound') result = result.filter((c) => c.callDirection === 'OUTBOUND');
+ else if (filter === 'missed') result = result.filter((c) => c.callStatus === 'MISSED');
+
+ // Search filter
+ if (search.trim()) {
+ const q = search.toLowerCase();
+ result = result.filter((c) => {
+ const name = c.leadName ?? leadNameMap.get(c.leadId ?? '') ?? '';
+ const phone = c.callerNumber?.[0]?.number ?? '';
+ const agent = c.agentName ?? '';
+ return (
+ name.toLowerCase().includes(q) ||
+ phone.includes(q) ||
+ agent.toLowerCase().includes(q)
+ );
+ });
+ }
+
+ return result;
+ }, [calls, filter, search, leadNameMap]);
+
+ const inboundCount = calls.filter((c) => c.callDirection === 'INBOUND').length;
+ const outboundCount = calls.filter((c) => c.callDirection === 'OUTBOUND').length;
+ const missedCount = calls.filter((c) => c.callStatus === 'MISSED').length;
+
return (
-
+
+
- {agentCalls.length === 0 ? (
-
-
+
+
+
+ setFilter(key as FilterKey)}
+ items={filterItems}
+ aria-label="Filter calls"
+ >
+ {(item) => (
+
+ {item.label}
+
+ )}
+
+
+
+ setSearch(value)}
+ aria-label="Search calls"
+ />
+
+
+ }
+ />
+
+ {filteredCalls.length === 0 ? (
+
No calls found
-
No call history available for your account yet.
+
+ {search ? 'Try a different search term' : 'No call history available yet.'}
+
-
- ) : (
-
-
-
-
-
- Date / Time
-
-
- Caller
-
-
- Lead Name
-
-
- Duration
-
-
- Disposition
-
-
-
-
- {agentCalls.map((call) => (
-
-
- {call.startedAt ? formatShortDate(call.startedAt) : '—'}
-
-
- {formatCallerNumber(call.callerNumber)}
-
-
- {call.leadName ?? '—'}
-
-
- {formatDuration(call.durationSeconds)}
-
-
- {call.disposition ? (
-
- {formatDispositionLabel(call.disposition)}
-
- ) : (
- —
- )}
-
-
- ))}
-
-
-
- )}
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(call) => {
+ const patientName = call.leadName ?? leadNameMap.get(call.leadId ?? '') ?? 'Unknown';
+ const phoneDisplay = formatPhoneDisplay(call);
+ const phoneRaw = call.callerNumber?.[0]?.number ?? '';
+ const dispositionCfg = call.disposition !== null ? dispositionConfig[call.disposition] : null;
+
+ return (
+
+
+
+
+
+
+ {patientName}
+
+
+
+
+ {phoneDisplay}
+
+
+
+
+ {formatDuration(call.durationSeconds)}
+
+
+
+ {dispositionCfg ? (
+
+ {dispositionCfg.label}
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+
+ {call.agentName ?? '\u2014'}
+
+
+
+ {call.recordingUrl ? (
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+
+ {call.startedAt ? formatShortDate(call.startedAt) : '\u2014'}
+
+
+
+ {phoneRaw ? (
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+ );
+ }}
+
+
+ )}
+
);
diff --git a/src/pages/patients.tsx b/src/pages/patients.tsx
new file mode 100644
index 0000000..fe58cd6
--- /dev/null
+++ b/src/pages/patients.tsx
@@ -0,0 +1,253 @@
+import { useMemo, useState } from 'react';
+import { useNavigate } from 'react-router';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faUser } from '@fortawesome/pro-duotone-svg-icons';
+import { SearchLg } from '@untitledui/icons';
+import { Avatar } from '@/components/base/avatar/avatar';
+import { Badge } from '@/components/base/badges/badges';
+import { Button } from '@/components/base/buttons/button';
+import { Input } from '@/components/base/input/input';
+import { Table, TableCard } from '@/components/application/table/table';
+import { TopBar } from '@/components/layout/top-bar';
+import { ClickToCallButton } from '@/components/call-desk/click-to-call-button';
+import { useData } from '@/providers/data-provider';
+import { getInitials } from '@/lib/format';
+import type { Patient } from '@/types/entities';
+
+const computeAge = (dateOfBirth: string | null): number | null => {
+ if (!dateOfBirth) return null;
+ const dob = new Date(dateOfBirth);
+ const today = new Date();
+ let age = today.getFullYear() - dob.getFullYear();
+ const monthDiff = today.getMonth() - dob.getMonth();
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
+ age--;
+ }
+ return age;
+};
+
+const formatGender = (gender: string | null): string => {
+ if (!gender) return '';
+ switch (gender) {
+ case 'MALE': return 'M';
+ case 'FEMALE': return 'F';
+ case 'OTHER': return 'O';
+ default: return '';
+ }
+};
+
+const getPatientDisplayName = (patient: Patient): string => {
+ if (patient.fullName) {
+ return `${patient.fullName.firstName} ${patient.fullName.lastName}`.trim();
+ }
+ return 'Unknown';
+};
+
+const getPatientPhone = (patient: Patient): string => {
+ return patient.phones?.primaryPhoneNumber ?? '';
+};
+
+const getPatientEmail = (patient: Patient): string => {
+ return patient.emails?.primaryEmail ?? '';
+};
+
+export const PatientsPage = () => {
+ const { patients, loading } = useData();
+ const navigate = useNavigate();
+ const [searchQuery, setSearchQuery] = useState('');
+ const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
+
+ const filteredPatients = useMemo(() => {
+ return patients.filter((patient) => {
+ // Search filter
+ if (searchQuery.trim()) {
+ const query = searchQuery.trim().toLowerCase();
+ const name = getPatientDisplayName(patient).toLowerCase();
+ const phone = getPatientPhone(patient).toLowerCase();
+ const email = getPatientEmail(patient).toLowerCase();
+ if (!name.includes(query) && !phone.includes(query) && !email.includes(query)) {
+ return false;
+ }
+ }
+
+ // Status filter — treat all patients as active for now since we don't have a status field
+ if (statusFilter === 'inactive') return false;
+
+ return true;
+ });
+ }, [patients, searchQuery, statusFilter]);
+
+ return (
+
+
+
+
+
+
+ {/* Status filter buttons */}
+
+ {(['all', 'active', 'inactive'] as const).map((status) => (
+ setStatusFilter(status)}
+ className={`px-3 py-1.5 text-xs font-medium transition duration-100 ease-linear capitalize ${
+ statusFilter === status
+ ? 'bg-active text-brand-secondary'
+ : 'bg-primary text-tertiary hover:bg-primary_hover'
+ }`}
+ >
+ {status}
+
+ ))}
+
+
+
+ setSearchQuery(value)}
+ aria-label="Search patients"
+ />
+
+
+ }
+ />
+
+ {loading ? (
+
+ ) : filteredPatients.length === 0 ? (
+
+
+
No patients found
+
+ {searchQuery ? 'Try adjusting your search.' : 'No patient records available yet.'}
+
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+ {(patient) => {
+ const displayName = getPatientDisplayName(patient);
+ const age = computeAge(patient.dateOfBirth);
+ const gender = formatGender(patient.gender);
+ const phone = getPatientPhone(patient);
+ const email = getPatientEmail(patient);
+ const initials = patient.fullName
+ ? getInitials(patient.fullName.firstName, patient.fullName.lastName)
+ : '?';
+
+ return (
+
+ {/* Patient name + avatar */}
+
+
+
+
+
+ {displayName}
+
+ {(age !== null || gender) && (
+
+ {[
+ age !== null ? `${age}y` : null,
+ gender || null,
+ ].filter(Boolean).join(' / ')}
+
+ )}
+
+
+
+
+ {/* Contact */}
+
+
+ {phone ? (
+ {phone}
+ ) : (
+ No phone
+ )}
+ {email ? (
+ {email}
+ ) : null}
+
+
+
+ {/* Type */}
+
+ {patient.patientType ? (
+
+ {patient.patientType}
+
+ ) : (
+ —
+ )}
+
+
+ {/* Gender */}
+
+
+ {patient.gender ? patient.gender.charAt(0) + patient.gender.slice(1).toLowerCase() : '—'}
+
+
+
+ {/* Age */}
+
+
+ {age !== null ? `${age} yrs` : '—'}
+
+
+
+ {/* Status */}
+
+
+ Active
+
+
+
+ {/* Actions */}
+
+
+ {phone && (
+
+ )}
+ navigate(`/patient/${patient.id}`)}
+ >
+ View
+
+
+
+
+ );
+ }}
+
+
+ )}
+
+
+
+ );
+};
diff --git a/src/pages/reports.tsx b/src/pages/reports.tsx
new file mode 100644
index 0000000..05666bf
--- /dev/null
+++ b/src/pages/reports.tsx
@@ -0,0 +1,347 @@
+import { useMemo } from 'react';
+import ReactECharts from 'echarts-for-react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faArrowTrendUp,
+ faArrowTrendDown,
+ faPhoneVolume,
+ faPhoneArrowDownLeft,
+ faPhoneArrowUpRight,
+ faPercent,
+} from '@fortawesome/pro-duotone-svg-icons';
+import { BadgeWithIcon } from '@/components/base/badges/badges';
+import { ArrowUp, ArrowDown } from '@untitledui/icons';
+import { TopBar } from '@/components/layout/top-bar';
+import { useData } from '@/providers/data-provider';
+import type { Call } from '@/types/entities';
+
+// Chart color palette — hardcoded from CSS tokens so ECharts can use them
+const COLORS = {
+ brand600: 'rgb(21, 112, 239)',
+ brand500: 'rgb(59, 130, 246)',
+ gray300: 'rgb(213, 215, 218)',
+ gray400: 'rgb(164, 167, 174)',
+ success500: 'rgb(23, 178, 106)',
+ warning500: 'rgb(247, 144, 9)',
+ error500: 'rgb(240, 68, 56)',
+ purple500: 'rgb(158, 119, 237)',
+};
+
+// Helpers
+
+const getLast7Days = (): { label: string; dateKey: string }[] => {
+ const days: { label: string; dateKey: string }[] = [];
+ const now = new Date();
+
+ for (let i = 6; i >= 0; i--) {
+ const date = new Date(now);
+ date.setDate(now.getDate() - i);
+ const label = date.toLocaleDateString('en-IN', { weekday: 'short', day: 'numeric' });
+ const dateKey = date.toISOString().slice(0, 10);
+ days.push({ label, dateKey });
+ }
+
+ return days;
+};
+
+const groupCallsByDate = (calls: Call[]): Record
=> {
+ const grouped: Record = {};
+
+ for (const call of calls) {
+ const dateStr = call.startedAt ?? call.createdAt;
+ if (!dateStr) continue;
+ const dateKey = new Date(dateStr).toISOString().slice(0, 10);
+
+ if (!grouped[dateKey]) {
+ grouped[dateKey] = { inbound: 0, outbound: 0 };
+ }
+
+ if (call.callDirection === 'INBOUND') {
+ grouped[dateKey].inbound++;
+ } else if (call.callDirection === 'OUTBOUND') {
+ grouped[dateKey].outbound++;
+ }
+ }
+
+ return grouped;
+};
+
+const computeTrendPercent = (current: number, previous: number): number => {
+ if (previous === 0) return current > 0 ? 100 : 0;
+ return Math.round(((current - previous) / previous) * 100);
+};
+
+// Components
+
+type KpiCardProps = {
+ label: string;
+ value: string | number;
+ trend: number;
+ icon: typeof faPhoneVolume;
+ iconColor: string;
+};
+
+const KpiCard = ({ label, value, trend, icon, iconColor }: KpiCardProps) => {
+ const isPositive = trend >= 0;
+
+ return (
+
+
+ {label}
+
+
+
+ {value}
+
+ {Math.abs(trend)}%
+
+
+
vs previous 7 days
+
+ );
+};
+
+export const ReportsPage = () => {
+ const { calls, loading } = useData();
+
+ // Split current 7 days vs previous 7 days
+ const { currentCalls, previousCalls } = useMemo(() => {
+ const now = new Date();
+ const sevenDaysAgo = new Date(now);
+ sevenDaysAgo.setDate(now.getDate() - 7);
+ const fourteenDaysAgo = new Date(now);
+ fourteenDaysAgo.setDate(now.getDate() - 14);
+
+ const current: Call[] = [];
+ const previous: Call[] = [];
+
+ for (const call of calls) {
+ const dateStr = call.startedAt ?? call.createdAt;
+ if (!dateStr) continue;
+ const date = new Date(dateStr);
+
+ if (date >= sevenDaysAgo) {
+ current.push(call);
+ } else if (date >= fourteenDaysAgo) {
+ previous.push(call);
+ }
+ }
+
+ return { currentCalls: current, previousCalls: previous };
+ }, [calls]);
+
+ // KPI values
+ const kpis = useMemo(() => {
+ const totalCurrent = currentCalls.length;
+ const totalPrevious = previousCalls.length;
+
+ const inboundCurrent = currentCalls.filter((c) => c.callDirection === 'INBOUND').length;
+ const inboundPrevious = previousCalls.filter((c) => c.callDirection === 'INBOUND').length;
+
+ const outboundCurrent = currentCalls.filter((c) => c.callDirection === 'OUTBOUND').length;
+ const outboundPrevious = previousCalls.filter((c) => c.callDirection === 'OUTBOUND').length;
+
+ const bookedCurrent = currentCalls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length;
+ const bookedPrevious = previousCalls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length;
+ const conversionCurrent = totalCurrent > 0 ? Math.round((bookedCurrent / totalCurrent) * 100) : 0;
+ const conversionPrevious = totalPrevious > 0 ? Math.round((bookedPrevious / totalPrevious) * 100) : 0;
+
+ return {
+ total: { value: totalCurrent, trend: computeTrendPercent(totalCurrent, totalPrevious) },
+ inbound: { value: inboundCurrent, trend: computeTrendPercent(inboundCurrent, inboundPrevious) },
+ outbound: { value: outboundCurrent, trend: computeTrendPercent(outboundCurrent, outboundPrevious) },
+ conversion: { value: conversionCurrent, trend: computeTrendPercent(conversionCurrent, conversionPrevious) },
+ };
+ }, [currentCalls, previousCalls]);
+
+ // Bar chart data — last 7 days
+ const barChartOption = useMemo(() => {
+ const days = getLast7Days();
+ const grouped = groupCallsByDate(calls);
+
+ const inboundData = days.map((d) => grouped[d.dateKey]?.inbound ?? 0);
+ const outboundData = days.map((d) => grouped[d.dateKey]?.outbound ?? 0);
+ const labels = days.map((d) => d.label);
+
+ return {
+ tooltip: {
+ trigger: 'axis' as const,
+ backgroundColor: '#fff',
+ borderColor: '#e5e7eb',
+ borderWidth: 1,
+ textStyle: { color: '#344054', fontSize: 12 },
+ },
+ legend: {
+ bottom: 0,
+ itemWidth: 12,
+ itemHeight: 12,
+ textStyle: { color: '#667085', fontSize: 12 },
+ data: ['Inbound', 'Outbound'],
+ },
+ grid: { left: 40, right: 16, top: 16, bottom: 40 },
+ xAxis: {
+ type: 'category' as const,
+ data: labels,
+ axisLine: { lineStyle: { color: '#e5e7eb' } },
+ axisTick: { show: false },
+ axisLabel: { color: '#667085', fontSize: 12 },
+ },
+ yAxis: {
+ type: 'value' as const,
+ splitLine: { lineStyle: { color: '#f2f4f7' } },
+ axisLabel: { color: '#667085', fontSize: 12 },
+ },
+ series: [
+ {
+ name: 'Inbound',
+ type: 'bar' as const,
+ data: inboundData,
+ barGap: '10%',
+ itemStyle: { color: COLORS.gray400, borderRadius: [4, 4, 0, 0] },
+ },
+ {
+ name: 'Outbound',
+ type: 'bar' as const,
+ data: outboundData,
+ itemStyle: { color: COLORS.brand600, borderRadius: [4, 4, 0, 0] },
+ },
+ ],
+ };
+ }, [calls]);
+
+ // Donut chart data — call outcomes
+ const { donutOption, donutTotal } = useMemo(() => {
+ const booked = currentCalls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length;
+ const followUp = currentCalls.filter((c) => c.disposition === 'FOLLOW_UP_SCHEDULED').length;
+ const infoOnly = currentCalls.filter((c) => c.disposition === 'INFO_PROVIDED').length;
+ const missed = currentCalls.filter((c) => c.callStatus === 'MISSED').length;
+ const other = currentCalls.length - booked - followUp - infoOnly - missed;
+ const total = currentCalls.length;
+
+ const data = [
+ { value: booked, name: 'Booked', itemStyle: { color: COLORS.success500 } },
+ { value: followUp, name: 'Follow-up', itemStyle: { color: COLORS.brand600 } },
+ { value: infoOnly, name: 'Info Only', itemStyle: { color: COLORS.purple500 } },
+ { value: missed, name: 'Missed', itemStyle: { color: COLORS.error500 } },
+ ...(other > 0 ? [{ value: other, name: 'Other', itemStyle: { color: COLORS.gray300 } }] : []),
+ ].filter((d) => d.value > 0);
+
+ const option = {
+ tooltip: {
+ trigger: 'item' as const,
+ backgroundColor: '#fff',
+ borderColor: '#e5e7eb',
+ borderWidth: 1,
+ textStyle: { color: '#344054', fontSize: 12 },
+ formatter: (params: { name: string; value: number; percent: number }) =>
+ `${params.name}: ${params.value} (${params.percent}%)`,
+ },
+ legend: {
+ bottom: 0,
+ itemWidth: 12,
+ itemHeight: 12,
+ textStyle: { color: '#667085', fontSize: 12 },
+ },
+ series: [
+ {
+ type: 'pie' as const,
+ radius: ['55%', '80%'],
+ center: ['50%', '45%'],
+ avoidLabelOverlap: false,
+ label: {
+ show: true,
+ position: 'center' as const,
+ formatter: () => `${total}`,
+ fontSize: 28,
+ fontWeight: 700,
+ color: '#101828',
+ },
+ emphasis: {
+ label: { show: true, fontSize: 28, fontWeight: 700 },
+ },
+ labelLine: { show: false },
+ data,
+ },
+ ],
+ };
+
+ return { donutOption: option, donutTotal: total };
+ }, [currentCalls]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {/* KPI Cards */}
+
+
+
+
+
+
+
+ {/* Charts row */}
+
+ {/* Call Volume Trend — 2/3 width */}
+
+
Call Volume Trend
+
Inbound vs outbound calls — last 7 days
+
+
+
+ {/* Call Outcomes Donut — 1/3 width */}
+
+
Call Outcomes
+
Disposition breakdown — last 7 days
+ {donutTotal === 0 ? (
+
+
No call data in this period
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/pages/team-dashboard.tsx b/src/pages/team-dashboard.tsx
index 674e77d..0ef7a9a 100644
--- a/src/pages/team-dashboard.tsx
+++ b/src/pages/team-dashboard.tsx
@@ -1,29 +1,456 @@
+import { useMemo, useState } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faPhone,
+ faPhoneArrowDownLeft,
+ faPhoneArrowUpRight,
+ faPhoneMissed,
+ faClock,
+ faCalendarCheck,
+ faUserHeadset,
+ faChartMixed,
+} from '@fortawesome/pro-duotone-svg-icons';
+import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { Avatar } from '@/components/base/avatar/avatar';
+import { Badge } from '@/components/base/badges/badges';
+import { Table, TableCard } from '@/components/application/table/table';
import { TopBar } from '@/components/layout/top-bar';
-import { TeamScoreboard } from '@/components/admin/team-scoreboard';
-import { CampaignRoiCards } from '@/components/admin/campaign-roi-cards';
-import { LeadFunnel } from '@/components/admin/lead-funnel';
-import { SlaMetrics } from '@/components/admin/sla-metrics';
-import { IntegrationHealth } from '@/components/admin/integration-health';
-import { useLeads } from '@/hooks/use-leads';
-import { useCampaigns } from '@/hooks/use-campaigns';
+import { AiChatPanel } from '@/components/call-desk/ai-chat-panel';
import { useData } from '@/providers/data-provider';
+import { getInitials, formatShortDate } from '@/lib/format';
+import type { Call } from '@/types/entities';
+
+// KPI Card component
+type KpiCardProps = {
+ label: string;
+ value: number | string;
+ icon: IconDefinition;
+ iconColor: string;
+ iconBg: string;
+ subtitle?: string;
+};
+
+const KpiCard = ({ label, value, icon, iconColor, iconBg, subtitle }: KpiCardProps) => (
+
+
+
+
+
+ {label}
+ {value}
+ {subtitle && {subtitle} }
+
+
+);
+
+// Metric card for performance row
+type MetricCardProps = {
+ label: string;
+ value: string;
+ description: string;
+};
+
+const MetricCard = ({ label, value, description }: MetricCardProps) => (
+
+ {label}
+ {value}
+ {description}
+
+);
+
+type DateRange = 'today' | 'week' | 'month';
+
+const getDateRangeStart = (range: DateRange): Date => {
+ const now = new Date();
+ switch (range) {
+ case 'today': {
+ const start = new Date(now);
+ start.setHours(0, 0, 0, 0);
+ return start;
+ }
+ case 'week': {
+ const start = new Date(now);
+ start.setDate(start.getDate() - 7);
+ return start;
+ }
+ case 'month': {
+ const start = new Date(now);
+ start.setDate(start.getDate() - 30);
+ return start;
+ }
+ }
+};
+
+const formatDuration = (seconds: number): string => {
+ if (seconds < 60) return `${seconds}s`;
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
+};
+
+const formatPercent = (value: number): string => {
+ if (isNaN(value) || !isFinite(value)) return '0%';
+ return `${Math.round(value)}%`;
+};
+
+type AgentPerformance = {
+ id: string;
+ name: string;
+ initials: string;
+ inboundCalls: number;
+ outboundCalls: number;
+ missedCalls: number;
+ totalCalls: number;
+ avgHandleTime: number;
+ appointmentsBooked: number;
+ conversionRate: number;
+};
export const TeamDashboardPage = () => {
- const { leads } = useLeads();
- const { campaigns } = useCampaigns();
- const { calls, agents, ingestionSources } = useData();
+ const { calls, leads, followUps, loading } = useData();
+ const [dateRange, setDateRange] = useState('week');
+
+ // Filter calls by date range
+ const filteredCalls = useMemo(() => {
+ const rangeStart = getDateRangeStart(dateRange);
+ return calls.filter((call) => {
+ if (!call.startedAt) return false;
+ return new Date(call.startedAt) >= rangeStart;
+ });
+ }, [calls, dateRange]);
+
+ // KPI computations
+ const totalCalls = filteredCalls.length;
+ const inboundCalls = filteredCalls.filter((c) => c.callDirection === 'INBOUND').length;
+ const outboundCalls = filteredCalls.filter((c) => c.callDirection === 'OUTBOUND').length;
+ const missedCalls = filteredCalls.filter((c) => c.callStatus === 'MISSED').length;
+
+ // Performance metrics
+ const avgResponseTime = useMemo(() => {
+ const leadsWithResponse = leads.filter((l) => l.createdAt && l.firstContactedAt);
+ if (leadsWithResponse.length === 0) return null;
+ const totalMinutes = leadsWithResponse.reduce((sum, l) => {
+ const created = new Date(l.createdAt!).getTime();
+ const contacted = new Date(l.firstContactedAt!).getTime();
+ return sum + (contacted - created) / 60000;
+ }, 0);
+ return Math.round(totalMinutes / leadsWithResponse.length);
+ }, [leads]);
+
+ const missedCallbackTime = useMemo(() => {
+ const missedCallsList = filteredCalls.filter((c) => c.callStatus === 'MISSED' && c.startedAt);
+ if (missedCallsList.length === 0) return null;
+ const now = Date.now();
+ const totalMinutes = missedCallsList.reduce((sum, c) => {
+ return sum + (now - new Date(c.startedAt!).getTime()) / 60000;
+ }, 0);
+ return Math.round(totalMinutes / missedCallsList.length);
+ }, [filteredCalls]);
+
+ const callToAppointmentRate = useMemo(() => {
+ if (totalCalls === 0) return 0;
+ const booked = filteredCalls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length;
+ return (booked / totalCalls) * 100;
+ }, [filteredCalls, totalCalls]);
+
+ const leadToAppointmentRate = useMemo(() => {
+ if (leads.length === 0) return 0;
+ const converted = leads.filter(
+ (l) => l.leadStatus === 'APPOINTMENT_SET' || l.leadStatus === 'CONVERTED',
+ ).length;
+ return (converted / leads.length) * 100;
+ }, [leads]);
+
+ // Agent performance table data
+ const agentPerformance = useMemo((): AgentPerformance[] => {
+ const agentMap = new Map();
+ for (const call of filteredCalls) {
+ const agent = call.agentName ?? 'Unknown';
+ if (!agentMap.has(agent)) agentMap.set(agent, []);
+ agentMap.get(agent)!.push(call);
+ }
+
+ return Array.from(agentMap.entries()).map(([name, agentCalls]) => {
+ const inbound = agentCalls.filter((c) => c.callDirection === 'INBOUND').length;
+ const outbound = agentCalls.filter((c) => c.callDirection === 'OUTBOUND').length;
+ const missed = agentCalls.filter((c) => c.callStatus === 'MISSED').length;
+ const total = agentCalls.length;
+ const totalDuration = agentCalls.reduce((sum, c) => sum + (c.durationSeconds ?? 0), 0);
+ const completedCalls = agentCalls.filter((c) => (c.durationSeconds ?? 0) > 0);
+ const avgHandle = completedCalls.length > 0 ? Math.round(totalDuration / completedCalls.length) : 0;
+ const booked = agentCalls.filter((c) => c.disposition === 'APPOINTMENT_BOOKED').length;
+ const conversion = total > 0 ? (booked / total) * 100 : 0;
+
+ const nameParts = name.split(' ');
+ const initials = getInitials(nameParts[0] ?? '', nameParts[1] ?? '');
+
+ return {
+ id: name,
+ name,
+ initials,
+ inboundCalls: inbound,
+ outboundCalls: outbound,
+ missedCalls: missed,
+ totalCalls: total,
+ avgHandleTime: avgHandle,
+ appointmentsBooked: booked,
+ conversionRate: conversion,
+ };
+ }).sort((a, b) => b.totalCalls - a.totalCalls);
+ }, [filteredCalls]);
+
+ // Missed call queue (recent missed calls)
+ const missedCallQueue = useMemo(() => {
+ return filteredCalls
+ .filter((c) => c.callStatus === 'MISSED')
+ .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;
+ })
+ .slice(0, 10);
+ }, [filteredCalls]);
+
+ const formatCallerPhone = (call: Call): string => {
+ if (!call.callerNumber || call.callerNumber.length === 0) return 'Unknown';
+ const first = call.callerNumber[0];
+ return `${first.callingCode} ${first.number}`;
+ };
+
+ const getTimeSince = (dateStr: string | null): string => {
+ if (!dateStr) return '—';
+ const diffMs = Date.now() - new Date(dateStr).getTime();
+ const mins = Math.floor(diffMs / 60000);
+ if (mins < 1) return 'Just now';
+ if (mins < 60) return `${mins}m ago`;
+ const hours = Math.floor(mins / 60);
+ if (hours < 24) return `${hours}h ago`;
+ return `${Math.floor(hours / 24)}d ago`;
+ };
+
+ // Supervisor AI quick prompts
+ const supervisorQuickPrompts = [
+ { label: 'Top conversions', template: 'Which agents have the highest conversion rates this week?' },
+ { label: 'Pending leads', template: 'How many leads are still pending first contact?' },
+ { label: 'Missed callback risks', template: 'Which missed calls have been waiting the longest without a callback?' },
+ { label: 'Weekly summary', template: 'Give me a summary of this week\'s team performance.' },
+ ];
+
+ const dateRangeLabel = dateRange === 'today' ? 'Today' : dateRange === 'week' ? 'This Week' : 'This Month';
return (
-
-
+
+
+
-
-
-
-
+ {/* Date range filter */}
+
+
Overview
+
+ {(['today', 'week', 'month'] as const).map((range) => (
+ setDateRange(range)}
+ className={`px-3 py-1.5 text-xs font-medium transition duration-100 ease-linear capitalize ${
+ dateRange === range
+ ? 'bg-active text-brand-secondary'
+ : 'bg-primary text-tertiary hover:bg-primary_hover'
+ }`}
+ >
+ {range === 'today' ? 'Today' : range === 'week' ? 'Week' : 'Month'}
+
+ ))}
+
+
+
+ {/* KPI Cards Row */}
+
+
+
+
+ 0 ? `${formatPercent((missedCalls / totalCalls) * 100)} of total` : undefined}
+ />
+
+
+ {/* Performance Metrics Row */}
+
+
+
+
+
+
+
+ {/* Agent Performance Table + Missed Call Queue */}
+
+ {/* Agent Performance Table */}
+
+
+
+ {loading ? (
+
+ ) : agentPerformance.length === 0 ? (
+
+
+
No agent data available
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+ {(agent) => (
+
+
+
+
+
+
+ {agent.name}
+
+
+ {agent.totalCalls} total calls
+
+
+
+
+
+ {agent.inboundCalls}
+
+
+ {agent.outboundCalls}
+
+
+ {agent.missedCalls > 0 ? (
+ {agent.missedCalls}
+ ) : (
+ 0
+ )}
+
+
+
+ {formatDuration(agent.avgHandleTime)}
+
+
+
+ = 30 ? 'success' : agent.conversionRate >= 15 ? 'warning' : 'gray'}
+ >
+ {formatPercent(agent.conversionRate)}
+
+
+
+ )}
+
+
+ )}
+
+
+
+ {/* Missed Call Queue */}
+
+
+
+
+
+
Missed Call Queue
+
+ {missedCalls > 0 && (
+
{missedCalls}
+ )}
+
+
+ {missedCallQueue.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* AI Assistant Section */}
+
+
+
+
Supervisor AI Assistant
+
+
-
-
);
diff --git a/src/providers/data-provider.tsx b/src/providers/data-provider.tsx
index 11e6d79..8ade3b9 100644
--- a/src/providers/data-provider.tsx
+++ b/src/providers/data-provider.tsx
@@ -8,6 +8,7 @@ import {
FOLLOW_UPS_QUERY,
LEAD_ACTIVITIES_QUERY,
CALLS_QUERY,
+ PATIENTS_QUERY,
} from '@/lib/queries';
import {
transformLeads,
@@ -16,9 +17,10 @@ import {
transformFollowUps,
transformLeadActivities,
transformCalls,
+ transformPatients,
} from '@/lib/transforms';
-import type { Lead, Campaign, Ad, LeadActivity, FollowUp, WhatsAppTemplate, Agent, Call, LeadIngestionSource } from '@/types/entities';
+import type { Lead, Campaign, Ad, LeadActivity, FollowUp, WhatsAppTemplate, Agent, Call, LeadIngestionSource, Patient } from '@/types/entities';
type DataContextType = {
leads: Lead[];
@@ -29,6 +31,7 @@ type DataContextType = {
templates: WhatsAppTemplate[];
agents: Agent[];
calls: Call[];
+ patients: Patient[];
ingestionSources: LeadIngestionSource[];
loading: boolean;
error: string | null;
@@ -60,6 +63,7 @@ export const DataProvider = ({ children }: DataProviderProps) => {
const [followUps, setFollowUps] = useState
([]);
const [leadActivities, setLeadActivities] = useState([]);
const [calls, setCalls] = useState([]);
+ const [patients, setPatients] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -80,13 +84,14 @@ export const DataProvider = ({ children }: DataProviderProps) => {
try {
const gql = (query: string) => apiClient.graphql(query, undefined, { silent: true }).catch(() => null);
- const [leadsData, campaignsData, adsData, followUpsData, activitiesData, callsData] = await Promise.all([
+ const [leadsData, campaignsData, adsData, followUpsData, activitiesData, callsData, patientsData] = await Promise.all([
gql(LEADS_QUERY),
gql(CAMPAIGNS_QUERY),
gql(ADS_QUERY),
gql(FOLLOW_UPS_QUERY),
gql(LEAD_ACTIVITIES_QUERY),
gql(CALLS_QUERY),
+ gql(PATIENTS_QUERY),
]);
if (leadsData) setLeads(transformLeads(leadsData));
@@ -95,6 +100,7 @@ export const DataProvider = ({ children }: DataProviderProps) => {
if (followUpsData) setFollowUps(transformFollowUps(followUpsData));
if (activitiesData) setLeadActivities(transformLeadActivities(activitiesData));
if (callsData) setCalls(transformCalls(callsData));
+ if (patientsData) setPatients(transformPatients(patientsData));
} catch (err: any) {
setError(err.message ?? 'Failed to load data');
} finally {
@@ -116,7 +122,7 @@ export const DataProvider = ({ children }: DataProviderProps) => {
return (
diff --git a/src/types/entities.ts b/src/types/entities.ts
index 53fd125..3a42666 100644
--- a/src/types/entities.ts
+++ b/src/types/entities.ts
@@ -278,6 +278,22 @@ export type Call = {
leadService?: string;
};
+// Patient domain
+export type PatientStatus = 'ACTIVE' | 'INACTIVE';
+export type PatientGender = 'MALE' | 'FEMALE' | 'OTHER';
+export type PatientType = 'OPD' | 'IPD' | 'EMERGENCY' | 'REGULAR';
+
+export type Patient = {
+ id: string;
+ createdAt: string | null;
+ fullName: { firstName: string; lastName: string } | null;
+ phones: { primaryPhoneNumber: string } | null;
+ emails: { primaryEmail: string } | null;
+ dateOfBirth: string | null;
+ gender: PatientGender | null;
+ patientType: PatientType | null;
+};
+
// Lead Ingestion Source domain
export type IntegrationStatus = 'ACTIVE' | 'WARNING' | 'ERROR' | 'DISABLED';
export type AuthStatus = 'VALID' | 'EXPIRING_SOON' | 'EXPIRED' | 'NOT_CONFIGURED';