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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
41
frontend/src/components/today/ChartCard.tsx
Normal file
41
frontend/src/components/today/ChartCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
83
frontend/src/components/today/TodayBarChart.tsx
Normal file
83
frontend/src/components/today/TodayBarChart.tsx
Normal 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)}…`;
|
||||
}
|
||||
62
frontend/src/components/today/TodayChartsSection.tsx
Normal file
62
frontend/src/components/today/TodayChartsSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
frontend/src/components/today/chart-theme.ts
Normal file
16
frontend/src/components/today/chart-theme.ts
Normal 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';
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user