Add Recharts bar charts to Today dashboard summary.

Extend the Today summary API with treatment mix and workflow task breakdowns, and render permission-aware chart cards on the Today page using Recharts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 00:34:38 +03:30
parent 22466490bf
commit 10959e3183
14 changed files with 754 additions and 22 deletions

View File

@@ -4,6 +4,7 @@ import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/navigation';
import { useAuth } from '@/lib/hooks/useAuth';
import { TodayKpiGrid } from '@/components/today/TodayKpiGrid';
import { TodayChartsSection } from '@/components/today/TodayChartsSection';
import { useTodaySummary } from '@/lib/hooks/useTodaySummary';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
@@ -39,6 +40,8 @@ export default function TodayPage() {
) : null}
<TodayKpiGrid widgets={data?.widgets ?? {}} loading={loading} />
<TodayChartsSection charts={data?.charts ?? {}} loading={loading} />
</div>
);
}

View File

@@ -0,0 +1,41 @@
import type { ReactNode } from 'react';
import { Card } from '@/components/ui/shared/Card';
interface ChartCardProps {
title: string;
subtitle?: string;
children: ReactNode;
emptyMessage?: string;
isEmpty?: boolean;
loading?: boolean;
}
export function ChartCard({
title,
subtitle,
children,
emptyMessage,
isEmpty = false,
loading = false,
}: ChartCardProps) {
return (
<Card className="min-h-[280px] flex flex-col">
<div className="mb-4">
<h2 className="text-base font-semibold text-card-foreground">{title}</h2>
{subtitle ? (
<p className="text-xs text-text-muted mt-1">{subtitle}</p>
) : null}
</div>
{loading ? (
<div className="flex-1 min-h-[220px] animate-pulse rounded-[var(--radius-md)] bg-background-secondary/60" />
) : isEmpty ? (
<div className="flex-1 min-h-[220px] flex items-center justify-center">
<p className="text-sm text-text-muted text-center px-4">{emptyMessage}</p>
</div>
) : (
<div className="flex-1 min-h-[220px]">{children}</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,83 @@
'use client';
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import type { TodayChartBucket } from '@/types/today';
import {
TODAY_CHART_AXIS_COLOR,
TODAY_CHART_COLORS,
TODAY_CHART_GRID_COLOR,
TODAY_CHART_TOOLTIP_BG,
TODAY_CHART_TOOLTIP_BORDER,
} from '@/components/today/chart-theme';
interface TodayBarChartProps {
data: TodayChartBucket[];
}
export function TodayBarChart({ data }: TodayBarChartProps) {
const chartData = data.map((item) => ({
...item,
shortLabel: truncateLabel(item.label),
}));
return (
<ResponsiveContainer width="100%" height={220}>
<BarChart
data={chartData}
margin={{ top: 8, right: 8, left: -12, bottom: 0 }}
>
<CartesianGrid stroke={TODAY_CHART_GRID_COLOR} vertical={false} />
<XAxis
dataKey="shortLabel"
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
tickLine={false}
interval={0}
/>
<YAxis
allowDecimals={false}
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
axisLine={false}
tickLine={false}
width={32}
/>
<Tooltip
cursor={{ fill: 'rgba(0, 188, 255, 0.08)' }}
contentStyle={{
backgroundColor: TODAY_CHART_TOOLTIP_BG,
border: `1px solid ${TODAY_CHART_TOOLTIP_BORDER}`,
borderRadius: '6px',
color: '#f5f9ff',
fontSize: '12px',
}}
labelFormatter={(_, payload) => {
const row = payload?.[0]?.payload as TodayChartBucket | undefined;
return row?.label ?? '';
}}
/>
<Bar dataKey="count" radius={[4, 4, 0, 0]} maxBarSize={48}>
{chartData.map((entry, index) => (
<Cell
key={entry.code}
fill={TODAY_CHART_COLORS[index % TODAY_CHART_COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
}
function truncateLabel(label: string, max = 12): string {
if (label.length <= max) return label;
return `${label.slice(0, max - 1)}`;
}

View File

@@ -0,0 +1,62 @@
'use client';
import { useTranslations } from 'next-intl';
import { useAuth } from '@/lib/hooks/useAuth';
import {
canViewTasks,
canViewTreatment,
} from '@/components/shared/permissions';
import { ChartCard } from '@/components/today/ChartCard';
import { TodayBarChart } from '@/components/today/TodayBarChart';
import type { TodaySummaryCharts } from '@/types/today';
interface TodayChartsSectionProps {
charts: TodaySummaryCharts;
loading?: boolean;
}
export function TodayChartsSection({ charts, loading = false }: TodayChartsSectionProps) {
const t = useTranslations('today');
const { currentOrganization } = useAuth();
const orgType = currentOrganization?.type;
const showTreatmentMix =
orgType === 'CLINIC' && canViewTreatment(currentOrganization);
const showTasksByStep =
orgType === 'LAB' && canViewTasks(currentOrganization);
if (!showTreatmentMix && !showTasksByStep) {
return null;
}
const treatmentData = charts.treatmentMixWeek ?? [];
const tasksData = charts.tasksByWorkflowStep ?? [];
return (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{showTreatmentMix ? (
<ChartCard
title={t('chartTreatmentMixTitle')}
subtitle={t('chartTreatmentMixSubtitle')}
loading={loading}
isEmpty={!loading && treatmentData.length === 0}
emptyMessage={t('chartEmpty')}
>
<TodayBarChart data={treatmentData} />
</ChartCard>
) : null}
{showTasksByStep ? (
<ChartCard
title={t('chartTasksByStepTitle')}
subtitle={t('chartTasksByStepSubtitle')}
loading={loading}
isEmpty={!loading && tasksData.length === 0}
emptyMessage={t('chartEmpty')}
>
<TodayBarChart data={tasksData} />
</ChartCard>
) : null}
</div>
);
}

View File

@@ -0,0 +1,16 @@
/** Bar fill colors aligned with the dark dashboard accent palette. */
export const TODAY_CHART_COLORS = [
'#00bcff',
'#e1bc72',
'#98d8b5',
'#cfb8f7',
'#f8c9a6',
'#f4b6b6',
'#abd8f3',
'#cbe9a1',
] as const;
export const TODAY_CHART_AXIS_COLOR = '#8ea3bf';
export const TODAY_CHART_GRID_COLOR = 'rgba(41, 69, 106, 0.55)';
export const TODAY_CHART_TOOLTIP_BG = '#14253d';
export const TODAY_CHART_TOOLTIP_BORDER = '#29456a';

View File

@@ -1,3 +1,14 @@
export type TodayChartBucket = {
code: string;
label: string;
count: number;
};
export type TodaySummaryCharts = {
treatmentMixWeek?: TodayChartBucket[];
tasksByWorkflowStep?: TodayChartBucket[];
};
export type TodayWidgetKey =
| 'appointmentsToday'
| 'patientsToday'
@@ -24,6 +35,7 @@ export interface TodaySummaryData {
orgType: 'CLINIC' | 'LAB';
range: { from: string; to: string };
widgets: TodaySummaryWidgets;
charts: TodaySummaryCharts;
}
export interface TodaySummaryResponse {