improvement: time period dropdown added to appropriate dashboard charts.

This commit is contained in:
2026-07-19 00:37:59 +03:30
parent 538121d653
commit db515f9630
18 changed files with 557 additions and 95 deletions

View File

@@ -30,3 +30,44 @@ export function mapWeekChartBuckets<T extends { code: string; label: string }>(
label: formatTodayChartDayLabel(bucket.code, formatter),
}));
}
/**
* Recharts XAxis spacing for dense day-series (e.g. 30-day month view).
* `preserveStartEnd` keeps first/last ticks; `minTickGap` skips overlaps.
*/
export function todayChartDenseXAxisProps(pointCount: number): {
interval: number | 'preserveStartEnd';
minTickGap?: number;
fontSize: number;
} {
if (pointCount <= 8) {
return { interval: 0, fontSize: 11 };
}
if (pointCount <= 14) {
return { interval: 1, fontSize: 10 };
}
return { interval: 'preserveStartEnd', minTickGap: 36, fontSize: 10 };
}
/**
* Category bar charts (prosthesis / treatment mix) — fewer bars but longer labels.
*/
export function todayChartCategoryXAxisProps(pointCount: number): {
interval: number | 'preserveStartEnd';
minTickGap?: number;
fontSize: number;
truncateMax: number;
} {
if (pointCount <= 4) {
return { interval: 0, fontSize: 11, truncateMax: 12 };
}
if (pointCount <= 6) {
return { interval: 1, fontSize: 10, truncateMax: 9, minTickGap: 28 };
}
return {
interval: 'preserveStartEnd',
minTickGap: 40,
fontSize: 10,
truncateMax: 8,
};
}

View File

@@ -0,0 +1,103 @@
'use client';
export type TodayChartPeriod = 'week' | 'month' | 'year';
export type TodayChartPeriodKey =
| 'appointmentsWeekAll'
| 'appointmentsWeekMine'
| 'labTaskActivity'
| 'casesDue'
| 'treatmentMix'
| 'casePartners'
| 'efficiency';
export type TodayChartPeriods = Record<TodayChartPeriodKey, TodayChartPeriod>;
export const TODAY_CHART_PERIOD_KEYS: TodayChartPeriodKey[] = [
'appointmentsWeekAll',
'appointmentsWeekMine',
'labTaskActivity',
'casesDue',
'treatmentMix',
'casePartners',
'efficiency',
];
export const TODAY_CHART_PERIOD_DEFAULTS: TodayChartPeriods = {
appointmentsWeekAll: 'week',
appointmentsWeekMine: 'week',
labTaskActivity: 'week',
casesDue: 'week',
treatmentMix: 'week',
casePartners: 'month',
efficiency: 'month',
};
/** Aggregate charts that support year; day-series charts are week/month only. */
export const TODAY_CHART_PERIODS_WITH_YEAR: ReadonlySet<TodayChartPeriodKey> = new Set([
'treatmentMix',
'casePartners',
'efficiency',
]);
export function daysForTodayChartPeriod(period: TodayChartPeriod): 7 | 30 | 365 {
switch (period) {
case 'month':
return 30;
case 'year':
return 365;
default:
return 7;
}
}
export function allowedPeriodsForChart(
key: TodayChartPeriodKey,
): readonly TodayChartPeriod[] {
return TODAY_CHART_PERIODS_WITH_YEAR.has(key)
? (['week', 'month', 'year'] as const)
: (['week', 'month'] as const);
}
function storageKey(organizationId: string): string {
return `dyolink:today-chart-periods:${organizationId}`;
}
function normalizePeriod(
key: TodayChartPeriodKey,
value: unknown,
): TodayChartPeriod {
const allowed = allowedPeriodsForChart(key);
if (typeof value === 'string' && (allowed as readonly string[]).includes(value)) {
return value as TodayChartPeriod;
}
return TODAY_CHART_PERIOD_DEFAULTS[key];
}
export function loadTodayChartPeriods(organizationId: string): TodayChartPeriods {
if (typeof window === 'undefined') return { ...TODAY_CHART_PERIOD_DEFAULTS };
try {
const raw = window.localStorage.getItem(storageKey(organizationId));
if (!raw) return { ...TODAY_CHART_PERIOD_DEFAULTS };
const parsed = JSON.parse(raw) as Partial<Record<TodayChartPeriodKey, unknown>>;
const next = { ...TODAY_CHART_PERIOD_DEFAULTS };
for (const key of TODAY_CHART_PERIOD_KEYS) {
next[key] = normalizePeriod(key, parsed[key]);
}
return next;
} catch {
return { ...TODAY_CHART_PERIOD_DEFAULTS };
}
}
export function saveTodayChartPeriods(
organizationId: string,
periods: TodayChartPeriods,
): void {
if (typeof window === 'undefined') return;
try {
window.localStorage.setItem(storageKey(organizationId), JSON.stringify(periods));
} catch {
// ignore quota / private mode
}
}

View File

@@ -5,25 +5,48 @@ import { ChartCardSkeleton } from '@/components/ui/today/TodaySkeleton';
interface ChartCardProps {
title: string;
subtitle?: string;
/** Compact control in the title row (e.g. period select) — does not grow card height. */
headerAction?: ReactNode;
children: ReactNode;
emptyMessage?: string;
isEmpty?: boolean;
loading?: boolean;
/**
* Two-column layout: left 2/3 (header + children), right 1/3 (chartPanel).
* Chart column is independent and vertically centered.
* Three-column layout for chart + legend gadgets:
* 1) title + legends, 2) centered chart panel, 3) header action (period select).
* Column order is grid flow — RTL reverses naturally with `dir`.
*/
sidePanelLayout?: boolean;
chartPanel?: ReactNode;
}
function ChartCardHeader({
function ChartCardTitleBlock({
title,
subtitle,
}: Pick<ChartCardProps, 'title' | 'subtitle'>) {
return (
<div className="mb-3 shrink-0">
<h2 className="text-base font-semibold text-card-foreground">{title}</h2>
<h2 className="min-w-0 truncate text-base font-semibold text-card-foreground">
{title}
</h2>
{subtitle ? <p className="mt-1 text-xs text-text-muted">{subtitle}</p> : null}
</div>
);
}
function ChartCardHeader({
title,
subtitle,
headerAction,
}: Pick<ChartCardProps, 'title' | 'subtitle' | 'headerAction'>) {
return (
<div className="mb-3 shrink-0">
<div className="flex items-center justify-between gap-2">
<h2 className="min-w-0 truncate text-base font-semibold text-card-foreground">
{title}
</h2>
{headerAction ? <div className="shrink-0">{headerAction}</div> : null}
</div>
{subtitle ? <p className="mt-1 text-xs text-text-muted">{subtitle}</p> : null}
</div>
);
@@ -32,6 +55,7 @@ function ChartCardHeader({
export function ChartCard({
title,
subtitle,
headerAction,
children,
emptyMessage,
isEmpty = false,
@@ -45,9 +69,10 @@ export function ChartCard({
if (sidePanelLayout) {
return (
<Card className="grid h-full min-h-0 grid-cols-[2fr_1fr] gap-x-3 overflow-hidden">
<Card className="grid h-full min-h-0 grid-cols-[minmax(0,1.1fr)_minmax(0,1.4fr)_auto] gap-x-3 overflow-hidden">
{/* Col 1 — title + legends (unchanged stacking) */}
<div className="flex min-h-0 flex-col overflow-hidden">
<ChartCardHeader title={title} subtitle={subtitle} />
<ChartCardTitleBlock title={title} subtitle={subtitle} />
{isEmpty ? (
<div className="flex min-h-0 flex-1 items-center justify-center">
<div className="flex w-full items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border/50 bg-background-secondary/20 py-8">
@@ -59,20 +84,28 @@ export function ChartCard({
)}
</div>
{/* Col 2 — pie / chart, centered */}
{!isEmpty && chartPanel ? (
<div className="flex min-h-0 items-center justify-center overflow-hidden py-1">
<div className="aspect-square h-full max-h-full w-full max-w-full">
{chartPanel}
</div>
</div>
) : null}
) : (
<div className="min-h-0" aria-hidden />
)}
{/* Col 3 — period select at top, aligned to the outer end like other gadgets */}
<div className="flex shrink-0 flex-col items-end self-start pt-0.5">
{headerAction}
</div>
</Card>
);
}
return (
<Card className="flex h-full min-h-0 flex-col overflow-hidden">
<ChartCardHeader title={title} subtitle={subtitle} />
<ChartCardHeader title={title} subtitle={subtitle} headerAction={headerAction} />
{isEmpty ? (
<div className="flex min-h-0 flex-1 items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border/50 bg-background-secondary/20">

View File

@@ -17,6 +17,7 @@ import {
TODAY_CHART_PRIMARY_COLOR,
TODAY_CHART_TOOLTIP_STYLE,
} from '@/components/today/chart-theme';
import { todayChartDenseXAxisProps } from '@/components/today/chart-day-labels';
interface TodayAreaChartProps {
data: TodayChartBucket[];
@@ -31,10 +32,12 @@ export function TodayAreaChart({
gradientId = 'todayAreaFill',
showXAxis = true,
}: TodayAreaChartProps) {
const xAxis = todayChartDenseXAxisProps(data.length);
return (
<TodayChartFrame>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 8, right: 8, left: -12, bottom: showXAxis ? 0 : -4 }}>
<AreaChart data={data} margin={{ top: 8, right: 8, left: -12, bottom: showXAxis ? 2 : -4 }}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.45} />
@@ -45,10 +48,11 @@ export function TodayAreaChart({
{showXAxis ? (
<XAxis
dataKey="label"
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: xAxis.fontSize }}
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
tickLine={false}
interval={1}
interval={xAxis.interval}
minTickGap={xAxis.minTickGap}
/>
) : (
<XAxis dataKey="label" hide />
@@ -71,7 +75,7 @@ export function TodayAreaChart({
stroke={color}
strokeWidth={2}
fill={`url(#${gradientId})`}
dot={{ r: 3, fill: color, strokeWidth: 0 }}
dot={data.length <= 14 ? { r: 3, fill: color, strokeWidth: 0 } : false}
activeDot={{ r: 5, fill: color }}
/>
</AreaChart>

View File

@@ -20,6 +20,7 @@ import {
TODAY_CHART_TOOLTIP_BG,
TODAY_CHART_TOOLTIP_BORDER,
} from '@/components/today/chart-theme';
import { todayChartCategoryXAxisProps } from '@/components/today/chart-day-labels';
interface TodayBarChartProps {
data: TodayChartBucket[];
@@ -28,9 +29,10 @@ interface TodayBarChartProps {
}
export function TodayBarChart({ data, colorForCode, onBarClick }: TodayBarChartProps) {
const xAxis = todayChartCategoryXAxisProps(data.length);
const chartData = data.map((item) => ({
...item,
shortLabel: truncateLabel(item.label),
shortLabel: truncateLabel(item.label, xAxis.truncateMax),
}));
return (
@@ -61,20 +63,21 @@ export function TodayBarChart({ data, colorForCode, onBarClick }: TodayBarChartP
<text
x={x}
y={y}
dy={16}
dy={14}
textAnchor="middle"
fill={fill}
fontSize={11}
fontSize={xAxis.fontSize}
>
{payload.value}
</text>
);
}
: { fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }
: { fill: TODAY_CHART_AXIS_COLOR, fontSize: xAxis.fontSize }
}
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
tickLine={false}
interval={0}
interval={xAxis.interval}
minTickGap={xAxis.minTickGap}
/>
<YAxis
allowDecimals={false}

View File

@@ -0,0 +1,51 @@
'use client';
import { useTranslations } from 'next-intl';
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
import type { TodayChartPeriod } from '@/components/today/chart-periods';
type TodayChartPeriodSelectProps = {
value: TodayChartPeriod;
options: readonly TodayChartPeriod[];
onChange: (period: TodayChartPeriod) => void;
disabled?: boolean;
id?: string;
};
export function TodayChartPeriodSelect({
value,
options,
onChange,
disabled = false,
id,
}: TodayChartPeriodSelectProps) {
const t = useTranslations('today');
const labelFor = (period: TodayChartPeriod) => {
switch (period) {
case 'month':
return t('periodMonth');
case 'year':
return t('periodYear');
default:
return t('periodWeek');
}
};
return (
<select
id={id}
value={value}
disabled={disabled}
aria-label={t('periodSelectAria')}
className={`${FORM_SELECT_CLASS} max-w-[7.5rem] shrink-0 py-0.5 text-xs`}
onChange={(event) => onChange(event.target.value as TodayChartPeriod)}
>
{options.map((period) => (
<option key={period} value={period}>
{labelFor(period)}
</option>
))}
</select>
);
}

View File

@@ -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<typeof getVisibleTodayKpis>;
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<typeof useTodayDayLabelFormatter>;
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) => (
<TodayChartPeriodSelect
value={chartPeriods[key]}
options={allowedPeriodsForChart(key)}
onChange={(period) => 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: (
<ChartCard
title={t('chartAppointmentsWeekAllTitle')}
subtitle={t('chartAppointmentsWeekAllSubtitle')}
headerAction={periodSelect('appointmentsWeekAll')}
isEmpty={appointmentsWeekAllData.every((row) => row.count === 0)}
emptyMessage={t('chartEmpty')}
>
@@ -488,7 +518,7 @@ function buildChartCells(options: {
content: (
<ChartCard
title={t('chartAppointmentsWeekMineTitle')}
subtitle={t('chartAppointmentsWeekMineSubtitle')}
headerAction={periodSelect('appointmentsWeekMine')}
isEmpty={appointmentsWeekMineData.every((row) => row.count === 0)}
emptyMessage={t('chartEmpty')}
>
@@ -505,7 +535,7 @@ function buildChartCells(options: {
content: (
<ChartCard
title={t('chartLabTaskActivityTitle')}
subtitle={t('chartLabTaskActivitySubtitle')}
headerAction={periodSelect('labTaskActivity')}
isEmpty={labTaskActivityData.every(
(row) => row.completed === 0 && row.received === 0,
)}
@@ -533,11 +563,7 @@ function buildChartCells(options: {
content: (
<ChartCard
title={t('chartEfficiencyReportTitle')}
subtitle={
orgType === 'CLINIC'
? t('chartEfficiencyReportSubtitleClinic')
: t('chartEfficiencyReportSubtitleLab')
}
headerAction={periodSelect('efficiency')}
isEmpty={efficiencyReportData.every((row) => row.count === 0)}
emptyMessage={t('chartEmpty')}
sidePanelLayout
@@ -588,7 +614,7 @@ function buildChartCells(options: {
content: (
<ChartCard
title={t('chartTreatmentMixTitle')}
subtitle={t('chartTreatmentMixSubtitle')}
headerAction={periodSelect('treatmentMix')}
isEmpty={treatmentData.length === 0}
emptyMessage={t('chartEmpty')}
>
@@ -638,7 +664,7 @@ function buildChartCells(options: {
content: (
<ChartCard
title={t('chartCasesDueWeekTitle')}
subtitle={t('chartCasesDueWeekSubtitle')}
headerAction={periodSelect('casesDue')}
isEmpty={casesDueWeekData.every((row) => 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,
)}

View File

@@ -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 (
<TodayChartFrame>
<div className="flex h-full min-h-0 flex-col">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data} margin={{ top: 8, right: 8, left: -12, bottom: 0 }}>
<AreaChart data={data} margin={{ top: 8, right: 8, left: -12, bottom: 2 }}>
<defs>
<linearGradient id="labTaskCompletedFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={TODAY_CHART_COMPLETED_COLOR} stopOpacity={0.4} />
@@ -55,10 +59,11 @@ export function TodayLabTaskActivityChart({
<CartesianGrid stroke={TODAY_CHART_GRID_COLOR} vertical={false} />
<XAxis
dataKey="label"
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: xAxis.fontSize }}
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
tickLine={false}
interval={1}
interval={xAxis.interval}
minTickGap={xAxis.minTickGap}
/>
<YAxis
allowDecimals={false}
@@ -79,7 +84,7 @@ export function TodayLabTaskActivityChart({
stroke={TODAY_CHART_COMPLETED_COLOR}
strokeWidth={2}
fill="url(#labTaskCompletedFill)"
dot={{ r: 3, fill: TODAY_CHART_COMPLETED_COLOR, strokeWidth: 0 }}
dot={showDots ? { r: 3, fill: TODAY_CHART_COMPLETED_COLOR, strokeWidth: 0 } : false}
activeDot={{ r: 5, fill: TODAY_CHART_COMPLETED_COLOR }}
/>
<Area
@@ -89,7 +94,7 @@ export function TodayLabTaskActivityChart({
stroke={TODAY_CHART_RECEIVED_COLOR}
strokeWidth={2}
fill="url(#labTaskReceivedFill)"
dot={{ r: 3, fill: TODAY_CHART_RECEIVED_COLOR, strokeWidth: 0 }}
dot={showDots ? { r: 3, fill: TODAY_CHART_RECEIVED_COLOR, strokeWidth: 0 } : false}
activeDot={{ r: 5, fill: TODAY_CHART_RECEIVED_COLOR }}
/>
</AreaChart>

View File

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

View File

@@ -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}
/>
<YAxis
allowDecimals={false}

View File

@@ -1,15 +1,33 @@
import { apiClient } from './client';
import type { TodaySummaryResponse } from '@/types/today';
import type { TodayChartPeriods } from '@/components/today/chart-periods';
export interface TodaySummaryParams {
from: string;
to: string;
utcOffsetMinutes?: number;
chartPeriods?: TodayChartPeriods;
}
function chartPeriodQuery(periods?: TodayChartPeriods) {
if (!periods) return {};
return {
appointmentsWeekPeriod: periods.appointmentsWeekAll,
appointmentsWeekMinePeriod: periods.appointmentsWeekMine,
labTaskActivityPeriod: periods.labTaskActivity,
casesDuePeriod: periods.casesDue,
treatmentMixPeriod: periods.treatmentMix,
casePartnersPeriod: periods.casePartners,
efficiencyPeriod: periods.efficiency,
};
}
export const todayApi = {
summary: async (params: TodaySummaryParams): Promise<TodaySummaryResponse> => {
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;
},
};

View File

@@ -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<void>;
}
@@ -19,6 +29,17 @@ export function useTodaySummary(organizationId?: string | null): UseTodaySummary
const [data, setData] = useState<TodaySummaryData | null>(null);
const [loading, setLoading] = useState(enabled);
const [error, setError] = useState<ApiError | null>(null);
const [chartPeriods, setChartPeriods] = useState<TodayChartPeriods>(() =>
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,
};
}