244 lines
8.3 KiB
TypeScript
244 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useAuth } from '@/lib/hooks/useAuth';
|
|
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 { TodayDonutChart } from '@/components/today/TodayDonutChart';
|
|
import { TodayHorizontalBarChart } from '@/components/today/TodayHorizontalBarChart';
|
|
import { TodayRadialGaugeChart } from '@/components/today/TodayRadialGaugeChart';
|
|
import { TodayStackedBarChart } from '@/components/today/TodayStackedBarChart';
|
|
import { ChartCardSkeleton } from '@/components/today/TodaySkeleton';
|
|
import { prosthesisTypeColor, prosthesisTypeSwatchStyle } from '@/components/ui/treatment/prosthesisTypeDisplay';
|
|
import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay';
|
|
import type { TodaySummaryCharts } from '@/types/today';
|
|
|
|
interface TodayChartsSectionProps {
|
|
charts: TodaySummaryCharts;
|
|
loading?: boolean;
|
|
isInitialLoad?: boolean;
|
|
className?: string;
|
|
/** When true, chart cards render as siblings (no outer grid wrapper). */
|
|
embedded?: boolean;
|
|
}
|
|
|
|
export function TodayChartsSection({
|
|
charts,
|
|
loading = false,
|
|
isInitialLoad = false,
|
|
className = '',
|
|
embedded = false,
|
|
}: TodayChartsSectionProps) {
|
|
const t = useTranslations('today');
|
|
const dayLabelFormatter = useTodayDayLabelFormatter();
|
|
const { currentOrganization } = useAuth();
|
|
const orgType = currentOrganization?.type;
|
|
|
|
const showAppointmentsByProvider =
|
|
orgType === 'CLINIC' && charts.appointmentsByProvider !== undefined;
|
|
const showAppointmentsWeekAll =
|
|
orgType === 'CLINIC' && charts.appointmentsWeekAll !== undefined;
|
|
const showAppointmentsWeekMine =
|
|
orgType === 'CLINIC' && charts.appointmentsWeekMine !== undefined;
|
|
const showTreatmentMix =
|
|
orgType === 'CLINIC' && charts.treatmentMixWeek !== undefined;
|
|
const showCaseCompletion =
|
|
orgType === 'LAB' && charts.caseCompletion !== undefined;
|
|
const showTasksByStep =
|
|
orgType === 'LAB' && charts.tasksByWorkflowStep !== undefined;
|
|
const showLabTaskActivityWeek =
|
|
orgType === 'LAB' && charts.labTaskActivityWeek !== undefined;
|
|
const showInProgressTasksByProsthesis =
|
|
orgType === 'LAB' && charts.inProgressTasksByProsthesis !== undefined;
|
|
|
|
const visibleChartCount =
|
|
Number(showAppointmentsByProvider) +
|
|
Number(showAppointmentsWeekAll) +
|
|
Number(showAppointmentsWeekMine) +
|
|
Number(showTreatmentMix) +
|
|
Number(showCaseCompletion) +
|
|
Number(showTasksByStep) +
|
|
Number(showLabTaskActivityWeek) +
|
|
Number(showInProgressTasksByProsthesis);
|
|
|
|
const appointmentsByProviderData = charts.appointmentsByProvider ?? [];
|
|
const appointmentsWeekAllData = useMemo(
|
|
() => mapWeekChartBuckets(charts.appointmentsWeekAll ?? [], dayLabelFormatter),
|
|
[charts.appointmentsWeekAll, dayLabelFormatter],
|
|
);
|
|
const appointmentsWeekMineData = useMemo(
|
|
() => mapWeekChartBuckets(charts.appointmentsWeekMine ?? [], dayLabelFormatter),
|
|
[charts.appointmentsWeekMine, dayLabelFormatter],
|
|
);
|
|
const treatmentData = charts.treatmentMixWeek ?? [];
|
|
const tasksData = charts.tasksByWorkflowStep ?? [];
|
|
const labTaskActivityData = useMemo(
|
|
() => mapWeekChartBuckets(charts.labTaskActivityWeek ?? [], dayLabelFormatter),
|
|
[charts.labTaskActivityWeek, dayLabelFormatter],
|
|
);
|
|
const prosthesisData = charts.inProgressTasksByProsthesis ?? [];
|
|
const caseCompletion = charts.caseCompletion ?? { completed: 0, total: 0, percent: 0 };
|
|
|
|
const formatDayLabel = (code: string) =>
|
|
formatTodayChartDayLabel(code, dayLabelFormatter);
|
|
|
|
if (visibleChartCount === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (isInitialLoad) {
|
|
const skeletons = Array.from({ length: Math.min(visibleChartCount, 4) }).map((_, index) => (
|
|
<ChartCardSkeleton key={index} />
|
|
));
|
|
|
|
if (embedded) {
|
|
return <>{skeletons}</>;
|
|
}
|
|
|
|
return (
|
|
<div className={`grid grid-cols-1 gap-4 lg:grid-cols-2 ${className}`}>{skeletons}</div>
|
|
);
|
|
}
|
|
|
|
const gridClass =
|
|
visibleChartCount > 1 ? 'grid grid-cols-1 lg:grid-cols-2' : 'grid grid-cols-1';
|
|
|
|
const chartCards = (
|
|
<>
|
|
{showAppointmentsWeekAll ? (
|
|
<ChartCard
|
|
title={t('chartAppointmentsWeekAllTitle')}
|
|
subtitle={t('chartAppointmentsWeekAllSubtitle')}
|
|
isEmpty={appointmentsWeekAllData.every((row) => row.count === 0)}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayAreaChart data={appointmentsWeekAllData} />
|
|
</ChartCard>
|
|
) : null}
|
|
|
|
{showAppointmentsWeekMine ? (
|
|
<ChartCard
|
|
title={t('chartAppointmentsWeekMineTitle')}
|
|
subtitle={t('chartAppointmentsWeekMineSubtitle')}
|
|
isEmpty={appointmentsWeekMineData.every((row) => row.count === 0)}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayAreaChart data={appointmentsWeekMineData} />
|
|
</ChartCard>
|
|
) : null}
|
|
|
|
{showLabTaskActivityWeek ? (
|
|
<ChartCard
|
|
title={t('chartLabTaskActivityTitle')}
|
|
subtitle={t('chartLabTaskActivitySubtitle')}
|
|
isEmpty={labTaskActivityData.every(
|
|
(row) => row.completed === 0 && row.received === 0,
|
|
)}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayStackedBarChart
|
|
data={labTaskActivityData}
|
|
completedLabel={t('chartLabTaskCompletedLegend')}
|
|
receivedLabel={t('chartLabTaskReceivedLegend')}
|
|
formatDayLabel={formatDayLabel}
|
|
/>
|
|
</ChartCard>
|
|
) : null}
|
|
|
|
{showInProgressTasksByProsthesis ? (
|
|
<ChartCard
|
|
title={t('chartProsthesisMixTitle')}
|
|
subtitle={t('chartProsthesisMixSubtitle')}
|
|
isEmpty={prosthesisData.length === 0}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayDonutChart
|
|
data={prosthesisData}
|
|
labelForCode={(code) =>
|
|
prosthesisData.find((row) => row.code === code)?.label ?? code
|
|
}
|
|
colorForCode={(code, index) => prosthesisTypeColor(code, index)}
|
|
swatchStyleForCode={(code, index) => prosthesisTypeSwatchStyle(code, index)}
|
|
variant="pie"
|
|
sideLegend
|
|
/>
|
|
</ChartCard>
|
|
) : null}
|
|
|
|
{showAppointmentsByProvider ? (
|
|
<ChartCard
|
|
title={t('chartAppointmentsByProviderTitle')}
|
|
subtitle={t('chartAppointmentsByProviderSubtitle')}
|
|
isEmpty={appointmentsByProviderData.length === 0}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayHorizontalBarChart data={appointmentsByProviderData} />
|
|
</ChartCard>
|
|
) : null}
|
|
|
|
{showTreatmentMix ? (
|
|
<ChartCard
|
|
title={t('chartTreatmentMixTitle')}
|
|
subtitle={t('chartTreatmentMixSubtitle')}
|
|
isEmpty={treatmentData.length === 0}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayBarChart
|
|
data={treatmentData}
|
|
colorForCode={(code, index) => treatmentTypeColor(code, index)}
|
|
/>
|
|
</ChartCard>
|
|
) : null}
|
|
|
|
{showCaseCompletion ? (
|
|
<ChartCard
|
|
title={t('chartCaseCompletionTitle')}
|
|
subtitle={t('chartCaseCompletionSubtitle')}
|
|
isEmpty={caseCompletion.total === 0}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayRadialGaugeChart
|
|
percent={caseCompletion.percent}
|
|
completed={caseCompletion.completed}
|
|
total={caseCompletion.total}
|
|
percentLabel={t('chartCaseCompletionPercent', {
|
|
percent: caseCompletion.percent,
|
|
})}
|
|
tasksLabel={t('chartCaseCompletionTasks')}
|
|
/>
|
|
</ChartCard>
|
|
) : null}
|
|
|
|
{showTasksByStep ? (
|
|
<ChartCard
|
|
title={t('chartTasksByStepTitle')}
|
|
subtitle={t('chartTasksByStepSubtitle')}
|
|
isEmpty={tasksData.length === 0}
|
|
emptyMessage={t('chartEmpty')}
|
|
>
|
|
<TodayBarChart data={tasksData} />
|
|
</ChartCard>
|
|
) : null}
|
|
</>
|
|
);
|
|
|
|
if (embedded) {
|
|
return chartCards;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`${gridClass} gap-4 ${loading ? 'opacity-70 transition-opacity' : ''} ${className}`}
|
|
>
|
|
{chartCards}
|
|
</div>
|
|
);
|
|
}
|