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 (

Loading data...

); } 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

) : ( )}
); };