'use client'; import { useMemo } from 'react'; import { useTranslations } from 'next-intl'; import { useAuth } from '@/lib/hooks/useAuth'; import { canEditCases, canEditTreatment, canViewAppointmentsTab, canViewCases, canViewLabCasesOrTasks, canViewMyAppointmentsWeekChart, 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 { mapWeekChartBuckets, useTodayDayLabelFormatter, } from '@/components/today/chart-day-labels'; import { TodayDashboardGrid } from '@/components/today/TodayDashboardGrid'; import { TodayDonutChart, TodayDonutChartLegend } from '@/components/today/TodayDonutChart'; import { TodayHorizontalBarChart } from '@/components/today/TodayHorizontalBarChart'; import { TodayPartnerCasesStackedBarChart } from '@/components/today/TodayPartnerCasesStackedBarChart'; import { Package, Stethoscope, type LucideIcon } from 'lucide-react'; import { TodayCompletionGaugeKpiCard } from '@/components/today/TodayCompletionGaugeKpiCard'; import { mapLabTaskActivityChartData, TodayLabTaskActivityChart, } from '@/components/today/TodayLabTaskActivityChart'; 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 } from '@/components/ui/treatment/prosthesisTypeDisplay'; import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay'; import type { TodayCompletionGauge, 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 && canViewMyAppointmentsWeekChart(currentOrganization); const showCasePartnersChart = Boolean( currentOrganization && ((orgType === 'CLINIC' && canEditTreatment(currentOrganization)) || (orgType === 'LAB' && canEditCases(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 showTreatmentPlanCompletionCard = orgType === 'CLINIC' && Boolean(currentOrganization && canEditTreatment(currentOrganization)) && (isInitialLoad || charts.treatmentPlanCompletion !== undefined); const cells = useMemo(() => { if (isInitialLoad) { return buildSkeletonCells({ kpiDefinitions, showSubscriptionCard, showCaseCompletionCard, showTreatmentPlanCompletionCard, showUpcoming: Boolean(showUpcoming), showCharts, orgType, isOwner, showMyAppointmentsWeekChart: Boolean( currentOrganization && canViewMyAppointmentsWeekChart(currentOrganization), ), showCasePartnersChart, charts, }); } return buildDashboardCells({ t, dayLabelFormatter, widgets, charts, actions, subscription, kpiDefinitions, showSubscriptionCard: showSubscriptionCard && Boolean(subscription), showCaseCompletionCard: showCaseCompletionCard && charts.caseCompletion !== undefined, showTreatmentPlanCompletionCard: showTreatmentPlanCompletionCard && charts.treatmentPlanCompletion !== undefined, showUpcoming: Boolean(showUpcoming), showCharts, orgType, isOwner, currentOrganization, }); }, [ isInitialLoad, kpiDefinitions, showSubscriptionCard, showCaseCompletionCard, showTreatmentPlanCompletionCard, 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; showTreatmentPlanCompletionCard: boolean; showUpcoming: boolean; showCharts: boolean; orgType?: 'CLINIC' | 'LAB'; isOwner: boolean; showMyAppointmentsWeekChart: boolean; showCasePartnersChart: boolean; charts: TodaySummaryCharts; }): TodayDashboardCell[] { const cells: TodayDashboardCell[] = []; if (options.showCharts) { const chartCount = countVisibleCharts( options.charts, options.orgType, options.isOwner, options.showMyAppointmentsWeekChart, options.showCasePartnersChart, ); 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: , }); } if (options.showTreatmentPlanCompletionCard) { cells.push({ id: 'treatment-plan-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; showTreatmentPlanCompletionCard: boolean; showUpcoming: boolean; showCharts: boolean; orgType?: 'CLINIC' | 'LAB'; isOwner: boolean; currentOrganization: ReturnType['currentOrganization']; }): TodayDashboardCell[] { const cells: TodayDashboardCell[] = []; if (options.showCharts) { cells.push( ...buildChartCells({ t: options.t, charts: options.charts, orgType: options.orgType, isOwner: options.isOwner, showMyAppointmentsWeekChart: Boolean( options.currentOrganization && canViewMyAppointmentsWeekChart(options.currentOrganization), ), showCasePartnersChart: Boolean(options.currentOrganization) && ((options.orgType === 'CLINIC' && canEditTreatment(options.currentOrganization)) || (options.orgType === 'LAB' && canEditCases(options.currentOrganization))), 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) { pushCompletionGaugeCell(cells, { id: 'case-completion', gauge: options.charts.caseCompletion, title: options.t('chartCaseCompletionTitle'), subtitle: options.t('chartCaseCompletionSubtitle'), percentLabel: options.t('chartCaseCompletionPercent', { percent: options.charts.caseCompletion.percent, }), ratioLabel: options.t('chartCaseCompletionTasks'), href: '/cases', icon: Package, }); } if ( options.showTreatmentPlanCompletionCard && options.charts.treatmentPlanCompletion !== undefined ) { pushCompletionGaugeCell(cells, { id: 'treatment-plan-completion', gauge: options.charts.treatmentPlanCompletion, title: options.t('chartTreatmentPlanCompletionTitle'), subtitle: options.t('chartTreatmentPlanCompletionSubtitle'), percentLabel: options.t('chartCaseCompletionPercent', { percent: options.charts.treatmentPlanCompletion.percent, }), ratioLabel: options.t('chartTreatmentPlanCompletionRatio'), href: '/appointments', icon: Stethoscope, }); } 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; showMyAppointmentsWeekChart: boolean; showCasePartnersChart: boolean; dayLabelFormatter: ReturnType; }): TodayDashboardCell[] { const { t, charts, orgType, isOwner, showMyAppointmentsWeekChart } = options; const cells: TodayDashboardCell[] = []; const areaChart = TODAY_DASHBOARD_LAYOUT.chartArea; const barChart = TODAY_DASHBOARD_LAYOUT.chartBar; const appointmentsWeekAllData = mapWeekChartBuckets( charts.appointmentsWeekAll ?? [], options.dayLabelFormatter, ); const appointmentsWeekMineData = mapWeekChartBuckets( charts.appointmentsWeekMine ?? [], options.dayLabelFormatter, ); const labTaskActivityData = mapWeekChartBuckets( charts.labTaskActivityWeek ?? [], options.dayLabelFormatter, ); const labTaskActivityChartData = mapLabTaskActivityChartData(labTaskActivityData); if (orgType === 'CLINIC' && charts.appointmentsWeekAll !== undefined) { cells.push({ id: 'chart-appointments-week-all', layout: areaChart, content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > ), }); } if ( orgType === 'CLINIC' && showMyAppointmentsWeekChart && charts.appointmentsWeekMine !== undefined ) { cells.push({ id: 'chart-appointments-week-mine', layout: areaChart, content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > ), }); } if (orgType === 'LAB' && charts.labTaskActivityWeek !== undefined) { cells.push({ id: 'chart-lab-task-activity', layout: areaChart, content: ( row.completed === 0 && row.received === 0, )} emptyMessage={t('chartEmpty')} > ), }); } const efficiencyReportData = charts.efficiencyReport ?? []; if ( isOwner && charts.efficiencyReport !== undefined && efficiencyReportData.length >= 2 ) { cells.push({ id: 'chart-efficiency-report', layout: areaChart, content: ( row.count === 0)} emptyMessage={t('chartEmpty')} sidePanelLayout chartPanel={ efficiencyReportData.find((row) => row.code === code)?.label ?? code } variant="pie" /> } > efficiencyReportData.find((row) => row.code === code)?.label ?? code } /> ), }); } const appointmentsByProviderData = charts.appointmentsByProvider ?? []; if (orgType === 'CLINIC' && charts.appointmentsByProvider !== undefined) { cells.push({ id: 'chart-appointments-by-provider', layout: barChart, content: ( ), }); } const treatmentData = charts.treatmentMixWeek ?? []; if (orgType === 'CLINIC' && charts.treatmentMixWeek !== undefined) { cells.push({ id: 'chart-treatment-mix', layout: barChart, content: ( treatmentTypeColor(code, index)} /> ), }); } const tasksByProsthesisData = charts.tasksByProsthesis ?? []; if (orgType === 'LAB' && charts.tasksByProsthesis !== undefined) { cells.push({ id: 'chart-tasks-by-prosthesis', layout: barChart, content: ( prosthesisTypeColor(code, index)} /> ), }); } const casePartnersData = charts.casePartnersMonth ?? []; if (options.showCasePartnersChart && charts.casePartnersMonth !== undefined) { cells.push({ id: 'chart-case-partners-month', layout: barChart, content: ( row.completed === 0 && row.pending === 0, )} emptyMessage={t('chartEmpty')} > ), }); } return cells; } function countVisibleCharts( charts: TodaySummaryCharts, orgType?: 'CLINIC' | 'LAB', isOwner = false, showMyAppointmentsWeekChart = false, showCasePartnersChart = false, ): number { let count = 0; if (orgType === 'CLINIC') { count += charts.appointmentsWeekAll !== undefined ? 1 : 0; count += showMyAppointmentsWeekChart && charts.appointmentsWeekMine !== undefined ? 1 : 0; count += charts.appointmentsByProvider !== undefined ? 1 : 0; count += charts.treatmentMixWeek !== undefined ? 1 : 0; count += showCasePartnersChart && charts.casePartnersMonth !== undefined ? 1 : 0; } if (orgType === 'LAB') { count += charts.labTaskActivityWeek !== undefined ? 1 : 0; count += charts.tasksByProsthesis !== undefined ? 1 : 0; count += showCasePartnersChart && charts.casePartnersMonth !== undefined ? 1 : 0; } if ( isOwner && charts.efficiencyReport !== undefined && (charts.efficiencyReport?.length ?? 0) >= 2 ) { count += 1; } return count; } function pushCompletionGaugeCell( cells: TodayDashboardCell[], options: { id: string; gauge: TodayCompletionGauge; title: string; subtitle: string; percentLabel: string; ratioLabel: string; href: string; icon: LucideIcon; }, ) { cells.push({ id: options.id, layout: TODAY_DASHBOARD_LAYOUT.subscription, content: ( ), }); }