diff --git a/.cursor/skills/today-dashboard/SKILL.md b/.cursor/skills/today-dashboard/SKILL.md index 8d5cbd1..9f3e541 100644 --- a/.cursor/skills/today-dashboard/SKILL.md +++ b/.cursor/skills/today-dashboard/SKILL.md @@ -35,10 +35,21 @@ Use `useRouter` from `@/i18n/navigation` for chart clicks. KPI cards use `Link`/ ## Charts (LAB examples) - **Tasks by prosthesis** — `TodayBarChart` + `colorForCode` from prosthesis catalog; `canViewTasks` -- **Cases due this week** — `casesDueWeek` buckets (next 7 local days); active sent cases with due date + in-progress task; `canViewCases`; empty card still shown -- **Lab task activity** — stacked week chart; `canViewCases \|\| canViewTasks` +- **Cases due** — forward day buckets (next 7 / 30); active sent cases with due date + in-progress task; `canViewCases`; empty card still shown +- **Lab task activity** — stacked lookback day chart (7 / 30); `canViewCases \|\| canViewTasks` -Week day labels: `useTodayDayLabelFormatter` + `mapWeekChartBuckets`. +Week/month day labels: `useTodayDayLabelFormatter` + `mapWeekChartBuckets`. + +## Chart period dropdowns + +Seven time-window charts support a compact header ` onChange(event.target.value as TodayChartPeriod)} + > + {options.map((period) => ( + + ))} + + ); +} diff --git a/frontend/src/components/ui/today/TodayDashboard.tsx b/frontend/src/components/ui/today/TodayDashboard.tsx index f7a920a..021c3e8 100644 --- a/frontend/src/components/ui/today/TodayDashboard.tsx +++ b/frontend/src/components/ui/today/TodayDashboard.tsx @@ -16,6 +16,13 @@ import { } from '@/components/shared/permissions'; import { KpiCard } from '@/components/ui/today/KpiCard'; import { ChartCard } from '@/components/ui/today/ChartCard'; +import { TodayChartPeriodSelect } from '@/components/ui/today/TodayChartPeriodSelect'; +import { + allowedPeriodsForChart, + type TodayChartPeriod, + type TodayChartPeriodKey, + type TodayChartPeriods, +} from '@/components/today/chart-periods'; import { TodayAreaChart } from '@/components/ui/today/TodayAreaChart'; import { TodayBarChart } from '@/components/ui/today/TodayBarChart'; import { @@ -62,6 +69,8 @@ interface TodayDashboardProps { charts: TodaySummaryCharts; actions: TodaySummaryActions; subscription?: TodaySubscriptionSnapshot; + chartPeriods: TodayChartPeriods; + onChartPeriodChange: (key: TodayChartPeriodKey, period: TodayChartPeriod) => void; loading?: boolean; isInitialLoad?: boolean; hasError?: boolean; @@ -72,6 +81,8 @@ export function TodayDashboard({ charts, actions, subscription, + chartPeriods, + onChartPeriodChange, loading = false, isInitialLoad = false, hasError = false, @@ -161,6 +172,8 @@ export function TodayDashboard({ charts, actions, subscription, + chartPeriods, + onChartPeriodChange, kpiDefinitions, showSubscriptionCard: showSubscriptionCard && Boolean(subscription), showCaseCompletionCard: @@ -195,6 +208,8 @@ export function TodayDashboard({ orgType, isOwner, charts, + chartPeriods, + onChartPeriodChange, t, dayLabelFormatter, widgets, @@ -314,6 +329,8 @@ function buildDashboardCells(options: { charts: TodaySummaryCharts; actions: TodaySummaryActions; subscription?: TodaySubscriptionSnapshot; + chartPeriods: TodayChartPeriods; + onChartPeriodChange: (key: TodayChartPeriodKey, period: TodayChartPeriod) => void; kpiDefinitions: ReturnType; showSubscriptionCard: boolean; showCaseCompletionCard: boolean; @@ -348,6 +365,8 @@ function buildDashboardCells(options: { canEditCases(options.currentOrganization))), dayLabelFormatter: options.dayLabelFormatter, prosthesisCatalog: options.prosthesisCatalog, + chartPeriods: options.chartPeriods, + onChartPeriodChange: options.onChartPeriodChange, onTasksProsthesisClick: options.onTasksProsthesisClick, onCasePartnerClick: options.onCasePartnerClick, }), @@ -438,10 +457,21 @@ function buildChartCells(options: { showCasePartnersChart: boolean; dayLabelFormatter: ReturnType; prosthesisCatalog: ProsthesisCatalogEntry[]; + chartPeriods: TodayChartPeriods; + onChartPeriodChange: (key: TodayChartPeriodKey, period: TodayChartPeriod) => void; onTasksProsthesisClick?: (code: string) => void; onCasePartnerClick?: (code: string) => void; }): TodayDashboardCell[] { - const { t, charts, orgType, isOwner, showMyAppointmentsWeekChart } = options; + const { t, charts, orgType, isOwner, showMyAppointmentsWeekChart, chartPeriods } = options; + + const periodSelect = (key: TodayChartPeriodKey) => ( + options.onChartPeriodChange(key, period)} + /> + ); + const cells: TodayDashboardCell[] = []; const areaChart = TODAY_DASHBOARD_LAYOUT.chartArea; const barChart = TODAY_DASHBOARD_LAYOUT.chartBar; @@ -467,7 +497,7 @@ function buildChartCells(options: { content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > @@ -488,7 +518,7 @@ function buildChartCells(options: { content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > @@ -505,7 +535,7 @@ function buildChartCells(options: { content: ( row.completed === 0 && row.received === 0, )} @@ -533,11 +563,7 @@ function buildChartCells(options: { content: ( row.count === 0)} emptyMessage={t('chartEmpty')} sidePanelLayout @@ -588,7 +614,7 @@ function buildChartCells(options: { content: ( @@ -638,7 +664,7 @@ function buildChartCells(options: { content: ( row.count === 0)} emptyMessage={t('chartEmpty')} > @@ -660,7 +686,7 @@ function buildChartCells(options: { ? t('chartCasePartnersClinicTitle') : t('chartCasePartnersLabTitle') } - subtitle={t('chartCasePartnersSubtitle')} + headerAction={periodSelect('casePartners')} isEmpty={casePartnersData.every( (row) => row.completed === 0 && row.pending === 0, )} diff --git a/frontend/src/components/ui/today/TodayLabTaskActivityChart.tsx b/frontend/src/components/ui/today/TodayLabTaskActivityChart.tsx index 2c5d9d2..92da160 100644 --- a/frontend/src/components/ui/today/TodayLabTaskActivityChart.tsx +++ b/frontend/src/components/ui/today/TodayLabTaskActivityChart.tsx @@ -17,6 +17,7 @@ import { TODAY_CHART_RECEIVED_COLOR, TODAY_CHART_TOOLTIP_STYLE, } from '@/components/today/chart-theme'; +import { todayChartDenseXAxisProps } from '@/components/today/chart-day-labels'; import type { TodayStackedDayBucket } from '@/types/today'; export type LabTaskActivityChartRow = { @@ -36,12 +37,15 @@ export function TodayLabTaskActivityChart({ completedLabel, receivedLabel, }: TodayLabTaskActivityChartProps) { + const xAxis = todayChartDenseXAxisProps(data.length); + const showDots = data.length <= 14; + return (
- + @@ -55,10 +59,11 @@ export function TodayLabTaskActivityChart({ diff --git a/frontend/src/components/ui/today/TodayPage.tsx b/frontend/src/components/ui/today/TodayPage.tsx index 0d294dd..ecc55f1 100644 --- a/frontend/src/components/ui/today/TodayPage.tsx +++ b/frontend/src/components/ui/today/TodayPage.tsx @@ -18,7 +18,15 @@ export function TodayPage() { const tErrors = useTranslations('errors'); const { currentOrganization } = useAuth(); const orgId = currentOrganization?.id; - const { data, loading, isInitialLoad, error, reload } = useTodaySummary(orgId); + const { + data, + loading, + isInitialLoad, + error, + chartPeriods, + setChartPeriod, + reload, + } = useTodaySummary(orgId); const showNoSubscriptionNotice = useMemo( () => Boolean(currentOrganization?.isOwner) && !currentOrganization?.plan, @@ -69,6 +77,8 @@ export function TodayPage() { charts={data?.charts ?? {}} actions={data?.actions ?? {}} subscription={data?.subscription} + chartPeriods={chartPeriods} + onChartPeriodChange={setChartPeriod} loading={loading} isInitialLoad={isInitialLoad} hasError={Boolean(error)} diff --git a/frontend/src/components/ui/today/TodayPartnerCasesStackedBarChart.tsx b/frontend/src/components/ui/today/TodayPartnerCasesStackedBarChart.tsx index ffe6042..9e4a4e3 100644 --- a/frontend/src/components/ui/today/TodayPartnerCasesStackedBarChart.tsx +++ b/frontend/src/components/ui/today/TodayPartnerCasesStackedBarChart.tsx @@ -55,7 +55,8 @@ export function TodayPartnerCasesStackedBarChart({ tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }} axisLine={{ stroke: TODAY_CHART_GRID_COLOR }} tickLine={false} - interval={0} + interval={chartData.length <= 8 ? 0 : 'preserveStartEnd'} + minTickGap={chartData.length > 8 ? 28 : undefined} /> => { - const response = await apiClient.get('/today/summary', { params }); + const { chartPeriods, ...range } = params; + const response = await apiClient.get('/today/summary', { + params: { ...range, ...chartPeriodQuery(chartPeriods) }, + }); return response.data; }, }; diff --git a/frontend/src/lib/hooks/useTodaySummary.ts b/frontend/src/lib/hooks/useTodaySummary.ts index 2fe78ea..ea874b8 100644 --- a/frontend/src/lib/hooks/useTodaySummary.ts +++ b/frontend/src/lib/hooks/useTodaySummary.ts @@ -2,6 +2,14 @@ import { useCallback, useEffect, useState } from 'react'; import { getLocalDayIsoRange } from '@/components/appointments/appointmentTime'; +import { + loadTodayChartPeriods, + saveTodayChartPeriods, + type TodayChartPeriod, + type TodayChartPeriodKey, + type TodayChartPeriods, + TODAY_CHART_PERIOD_DEFAULTS, +} from '@/components/today/chart-periods'; import { todayApi } from '@/lib/api/today'; import type { TodaySummaryData } from '@/types/today'; import type { ApiError } from '@/types/api'; @@ -11,6 +19,8 @@ interface UseTodaySummaryResult { loading: boolean; isInitialLoad: boolean; error: ApiError | null; + chartPeriods: TodayChartPeriods; + setChartPeriod: (key: TodayChartPeriodKey, period: TodayChartPeriod) => void; reload: () => Promise; } @@ -19,6 +29,17 @@ export function useTodaySummary(organizationId?: string | null): UseTodaySummary const [data, setData] = useState(null); const [loading, setLoading] = useState(enabled); const [error, setError] = useState(null); + const [chartPeriods, setChartPeriods] = useState(() => + organizationId ? loadTodayChartPeriods(organizationId) : TODAY_CHART_PERIOD_DEFAULTS, + ); + + useEffect(() => { + if (!organizationId) { + setChartPeriods(TODAY_CHART_PERIOD_DEFAULTS); + return; + } + setChartPeriods(loadTodayChartPeriods(organizationId)); + }, [organizationId]); const reload = useCallback(async () => { if (!organizationId) { @@ -34,14 +55,18 @@ export function useTodaySummary(organizationId?: string | null): UseTodaySummary try { const range = getLocalDayIsoRange(new Date()); const utcOffsetMinutes = -new Date().getTimezoneOffset(); - const response = await todayApi.summary({ ...range, utcOffsetMinutes }); + const response = await todayApi.summary({ + ...range, + utcOffsetMinutes, + chartPeriods, + }); setData(response.data); } catch (err) { setError(err as ApiError); } finally { setLoading(false); } - }, [organizationId]); + }, [organizationId, chartPeriods]); useEffect(() => { setData(null); @@ -54,11 +79,25 @@ export function useTodaySummary(organizationId?: string | null): UseTodaySummary void reload(); }, [organizationId, reload]); + const setChartPeriod = useCallback( + (key: TodayChartPeriodKey, period: TodayChartPeriod) => { + setChartPeriods((prev) => { + if (prev[key] === period) return prev; + const next = { ...prev, [key]: period }; + if (organizationId) saveTodayChartPeriods(organizationId, next); + return next; + }); + }, + [organizationId], + ); + return { data, loading, isInitialLoad: loading && !data, error, + chartPeriods, + setChartPeriod, reload, }; }