Files
dyolink/frontend/src/components/today/TodayChartsSection.tsx

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