'use client'; import { useMemo } from 'react'; import { useTranslations } from 'next-intl'; import { useAuth } from '@/lib/hooks/useAuth'; import { canViewAppointmentsTab, canViewCases, canViewLabCasesOrTasks, canViewTasks, canViewTreatment, } from '@/components/shared/permissions'; import { KpiCard } from '@/components/today/KpiCard'; import { ChartCard } from '@/components/today/ChartCard'; import { TodayAreaChart } from '@/components/today/TodayAreaChart'; import { TodayBarChart } from '@/components/today/TodayBarChart'; import { formatTodayChartDayLabel, mapWeekChartBuckets, useTodayDayLabelFormatter, } from '@/components/today/chart-day-labels'; import { TodayDashboardGrid } from '@/components/today/TodayDashboardGrid'; import { TodayDonutChart } from '@/components/today/TodayDonutChart'; import { TodayHorizontalBarChart } from '@/components/today/TodayHorizontalBarChart'; import { TodayCaseCompletionKpiCard } from '@/components/today/TodayCaseCompletionKpiCard'; import { TodayStackedBarChart } from '@/components/today/TodayStackedBarChart'; import { TodaySubscriptionKpiCard } from '@/components/today/TodaySubscriptionKpiCard'; import { TodayUpcomingAppointments } from '@/components/today/TodayUpcomingAppointments'; import { ChartCardSkeleton, KpiCardSkeleton, ListRowSkeleton, } from '@/components/today/TodaySkeleton'; import { TODAY_DASHBOARD_LAYOUT, type TodayDashboardCell, } from '@/components/today/today-dashboard-layout'; import { getEligibleTodayKpis, getVisibleTodayKpis } from '@/components/today/widget-registry'; import { prosthesisTypeColor, prosthesisTypeSwatchStyle } from '@/components/ui/treatment/prosthesisTypeDisplay'; import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay'; import type { TodaySubscriptionSnapshot, TodaySummaryActions, TodaySummaryCharts, TodaySummaryWidgets, } from '@/types/today'; interface TodayDashboardProps { widgets: TodaySummaryWidgets; charts: TodaySummaryCharts; actions: TodaySummaryActions; subscription?: TodaySubscriptionSnapshot; loading?: boolean; isInitialLoad?: boolean; hasError?: boolean; } export function TodayDashboard({ widgets, charts, actions, subscription, loading = false, isInitialLoad = false, hasError = false, }: TodayDashboardProps) { const t = useTranslations('today'); const dayLabelFormatter = useTodayDayLabelFormatter(); const { currentOrganization } = useAuth(); const orgType = currentOrganization?.type; const isOwner = Boolean(currentOrganization?.isOwner); const showUpcoming = orgType === 'CLINIC' && currentOrganization && canViewTreatment(currentOrganization); const showCharts = useMemo(() => { if (!orgType || !currentOrganization) return false; if (orgType === 'CLINIC') { return ( canViewAppointmentsTab(currentOrganization) || canViewTreatment(currentOrganization) ); } return ( canViewCases(currentOrganization) || canViewTasks(currentOrganization) || canViewLabCasesOrTasks(currentOrganization) ); }, [currentOrganization, orgType]); const kpiDefinitions = isInitialLoad ? getEligibleTodayKpis(currentOrganization) : getVisibleTodayKpis(currentOrganization, widgets); const showSubscriptionCard = isOwner && (isInitialLoad || Boolean(subscription)); const showCaseCompletionCard = orgType === 'LAB' && Boolean(currentOrganization && canViewCases(currentOrganization)) && (isInitialLoad || charts.caseCompletion !== undefined); const cells = useMemo(() => { if (isInitialLoad) { return buildSkeletonCells({ kpiDefinitions, showSubscriptionCard, showCaseCompletionCard, showUpcoming: Boolean(showUpcoming), showCharts, orgType, isOwner, charts, }); } return buildDashboardCells({ t, dayLabelFormatter, widgets, charts, actions, subscription, kpiDefinitions, showSubscriptionCard: showSubscriptionCard && Boolean(subscription), showCaseCompletionCard: showCaseCompletionCard && charts.caseCompletion !== undefined, showUpcoming: Boolean(showUpcoming), showCharts, orgType, isOwner, currentOrganization, }); }, [ isInitialLoad, kpiDefinitions, showSubscriptionCard, showCaseCompletionCard, showUpcoming, showCharts, orgType, isOwner, charts, t, dayLabelFormatter, widgets, actions, subscription, currentOrganization, ]); if (hasError && !loading && cells.length === 0) { return null; } if (!loading && !hasError && cells.length === 0) { return (

{t('noWidgets')}

); } return ; } function buildSkeletonCells(options: { kpiDefinitions: ReturnType; showSubscriptionCard: boolean; showCaseCompletionCard: boolean; showUpcoming: boolean; showCharts: boolean; orgType?: 'CLINIC' | 'LAB'; isOwner: boolean; charts: TodaySummaryCharts; }): TodayDashboardCell[] { const cells: TodayDashboardCell[] = []; if (options.showCharts) { const chartCount = countVisibleCharts(options.charts, options.orgType, options.isOwner); for (let index = 0; index < Math.min(chartCount, 4); index += 1) { cells.push({ id: `chart-skeleton-${index}`, layout: TODAY_DASHBOARD_LAYOUT.chart, content: , }); } } if (options.showUpcoming) { cells.push({ id: 'upcoming-skeleton', layout: TODAY_DASHBOARD_LAYOUT.upcoming, content: (
{[0, 1].map((key) => ( ))}
), }); } if (options.showSubscriptionCard) { cells.push({ id: 'subscription-skeleton', layout: TODAY_DASHBOARD_LAYOUT.subscription, content: , }); } if (options.showCaseCompletionCard) { cells.push({ id: 'case-completion-skeleton', layout: TODAY_DASHBOARD_LAYOUT.subscription, content: , }); } for (const definition of options.kpiDefinitions) { cells.push({ id: `kpi-skeleton-${definition.key}`, layout: TODAY_DASHBOARD_LAYOUT.kpi, content: , }); } return cells; } function buildDashboardCells(options: { t: ReturnType>; dayLabelFormatter: ReturnType; widgets: TodaySummaryWidgets; charts: TodaySummaryCharts; actions: TodaySummaryActions; subscription?: TodaySubscriptionSnapshot; kpiDefinitions: ReturnType; showSubscriptionCard: boolean; showCaseCompletionCard: boolean; showUpcoming: boolean; showCharts: boolean; orgType?: 'CLINIC' | 'LAB'; isOwner: boolean; currentOrganization: ReturnType['currentOrganization']; }): TodayDashboardCell[] { const cells: TodayDashboardCell[] = []; const formatDayLabel = (code: string) => formatTodayChartDayLabel(code, options.dayLabelFormatter); if (options.showCharts) { cells.push( ...buildChartCells({ t: options.t, charts: options.charts, orgType: options.orgType, isOwner: options.isOwner, formatDayLabel, dayLabelFormatter: options.dayLabelFormatter, }), ); } if (options.showUpcoming) { cells.push({ id: 'upcoming-appointments', layout: TODAY_DASHBOARD_LAYOUT.upcoming, content: ( ), }); } if (options.showSubscriptionCard && options.subscription) { cells.push({ id: 'subscription', layout: TODAY_DASHBOARD_LAYOUT.subscription, content: , }); } if (options.showCaseCompletionCard && options.charts.caseCompletion !== undefined) { const caseCompletion = options.charts.caseCompletion; cells.push({ id: 'case-completion', layout: TODAY_DASHBOARD_LAYOUT.subscription, content: ( ), }); } for (const definition of options.kpiDefinitions) { const value = definition.formatValue(options.widgets) ?? '—'; const subtitle = definition.formatSubtitle?.(options.widgets); cells.push({ id: `kpi-${definition.key}`, layout: TODAY_DASHBOARD_LAYOUT.kpi, content: ( ), }); } return cells; } function buildChartCells(options: { t: ReturnType>; charts: TodaySummaryCharts; orgType?: 'CLINIC' | 'LAB'; isOwner: boolean; formatDayLabel: (code: string) => string; dayLabelFormatter: ReturnType; }): TodayDashboardCell[] { const { t, charts, orgType, isOwner } = options; const cells: TodayDashboardCell[] = []; const tallChart = TODAY_DASHBOARD_LAYOUT.chart; const mediumChart = TODAY_DASHBOARD_LAYOUT.chartMedium; const appointmentsWeekAllData = mapWeekChartBuckets( charts.appointmentsWeekAll ?? [], options.dayLabelFormatter, ); const appointmentsWeekMineData = mapWeekChartBuckets( charts.appointmentsWeekMine ?? [], options.dayLabelFormatter, ); const labTaskActivityData = mapWeekChartBuckets( charts.labTaskActivityWeek ?? [], options.dayLabelFormatter, ); if (orgType === 'CLINIC' && charts.appointmentsWeekAll !== undefined) { cells.push({ id: 'chart-appointments-week-all', layout: tallChart, content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > ), }); } if (orgType === 'CLINIC' && charts.appointmentsWeekMine !== undefined) { cells.push({ id: 'chart-appointments-week-mine', layout: tallChart, content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > ), }); } if (orgType === 'LAB' && charts.labTaskActivityWeek !== undefined) { cells.push({ id: 'chart-lab-task-activity', layout: tallChart, content: ( row.completed === 0 && row.received === 0, )} emptyMessage={t('chartEmpty')} > ), }); } const prosthesisData = charts.inProgressTasksByProsthesis ?? []; if (orgType === 'LAB' && charts.inProgressTasksByProsthesis !== undefined) { cells.push({ id: 'chart-prosthesis-mix', layout: tallChart, content: ( prosthesisData.find((row) => row.code === code)?.label ?? code } colorForCode={(code, index) => prosthesisTypeColor(code, index)} swatchStyleForCode={(code, index) => prosthesisTypeSwatchStyle(code, index)} variant="pie" sideLegend /> ), }); } const efficiencyReportData = charts.efficiencyReport ?? []; if ( isOwner && charts.efficiencyReport !== undefined && efficiencyReportData.length >= 2 ) { cells.push({ id: 'chart-efficiency-report', layout: tallChart, content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > efficiencyReportData.find((row) => row.code === code)?.label ?? code } variant="pie" sideLegend /> ), }); } const appointmentsByProviderData = charts.appointmentsByProvider ?? []; if (orgType === 'CLINIC' && charts.appointmentsByProvider !== undefined) { cells.push({ id: 'chart-appointments-by-provider', layout: mediumChart, content: ( ), }); } const treatmentData = charts.treatmentMixWeek ?? []; if (orgType === 'CLINIC' && charts.treatmentMixWeek !== undefined) { cells.push({ id: 'chart-treatment-mix', layout: mediumChart, content: ( treatmentTypeColor(code, index)} /> ), }); } const tasksData = charts.tasksByWorkflowStep ?? []; if (orgType === 'LAB' && charts.tasksByWorkflowStep !== undefined) { cells.push({ id: 'chart-tasks-by-step', layout: mediumChart, content: ( ), }); } return cells; } function countVisibleCharts( charts: TodaySummaryCharts, orgType?: 'CLINIC' | 'LAB', isOwner = false, ): number { let count = 0; if (orgType === 'CLINIC') { count += charts.appointmentsWeekAll !== undefined ? 1 : 0; count += charts.appointmentsWeekMine !== undefined ? 1 : 0; count += charts.appointmentsByProvider !== undefined ? 1 : 0; count += charts.treatmentMixWeek !== undefined ? 1 : 0; } if (orgType === 'LAB') { count += charts.labTaskActivityWeek !== undefined ? 1 : 0; count += charts.inProgressTasksByProsthesis !== undefined ? 1 : 0; count += charts.tasksByWorkflowStep !== undefined ? 1 : 0; } if ( isOwner && charts.efficiencyReport !== undefined && (charts.efficiencyReport?.length ?? 0) >= 2 ) { count += 1; } return count; }