Some functionality added to dashboard gadgets.

This commit is contained in:
2026-07-12 00:28:43 +03:30
parent 83f6003a2b
commit d79aab907c
21 changed files with 557 additions and 412 deletions

View File

@@ -7,6 +7,7 @@ import {
canViewAppointmentsTab,
canViewCases,
canViewLabCasesOrTasks,
canViewMyAppointmentsWeekChart,
canViewTasks,
canViewTreatment,
} from '@/components/shared/permissions';
@@ -15,15 +16,17 @@ 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 { TodayDashboardGrid } from '@/components/today/TodayDashboardGrid';
import { TodayDonutChart } from '@/components/today/TodayDonutChart';
import { TodayDonutChart, TodayDonutChartLegend } from '@/components/today/TodayDonutChart';
import { TodayHorizontalBarChart } from '@/components/today/TodayHorizontalBarChart';
import { TodayCaseCompletionKpiCard } from '@/components/today/TodayCaseCompletionKpiCard';
import { TodayStackedBarChart } from '@/components/today/TodayStackedBarChart';
import {
mapLabTaskActivityChartData,
TodayLabTaskActivityChart,
} from '@/components/today/TodayLabTaskActivityChart';
import { TodaySubscriptionKpiCard } from '@/components/today/TodaySubscriptionKpiCard';
import { TodayUpcomingAppointments } from '@/components/today/TodayUpcomingAppointments';
import {
@@ -36,7 +39,7 @@ import {
type TodayDashboardCell,
} from '@/components/today/today-dashboard-layout';
import { getEligibleTodayKpis, getVisibleTodayKpis } from '@/components/today/widget-registry';
import { prosthesisTypeColor, prosthesisTypeSwatchStyle } from '@/components/ui/treatment/prosthesisTypeDisplay';
import { prosthesisTypeColor } from '@/components/ui/treatment/prosthesisTypeDisplay';
import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay';
import type {
TodaySubscriptionSnapshot,
@@ -71,7 +74,9 @@ export function TodayDashboard({
const isOwner = Boolean(currentOrganization?.isOwner);
const showUpcoming =
orgType === 'CLINIC' && currentOrganization && canViewTreatment(currentOrganization);
orgType === 'CLINIC' &&
currentOrganization &&
canViewMyAppointmentsWeekChart(currentOrganization);
const showCharts = useMemo(() => {
if (!orgType || !currentOrganization) return false;
@@ -109,6 +114,10 @@ export function TodayDashboard({
showCharts,
orgType,
isOwner,
showMyAppointmentsWeekChart: Boolean(
currentOrganization &&
canViewMyAppointmentsWeekChart(currentOrganization),
),
charts,
});
}
@@ -171,12 +180,18 @@ function buildSkeletonCells(options: {
showCharts: boolean;
orgType?: 'CLINIC' | 'LAB';
isOwner: boolean;
showMyAppointmentsWeekChart: boolean;
charts: TodaySummaryCharts;
}): TodayDashboardCell[] {
const cells: TodayDashboardCell[] = [];
if (options.showCharts) {
const chartCount = countVisibleCharts(options.charts, options.orgType, options.isOwner);
const chartCount = countVisibleCharts(
options.charts,
options.orgType,
options.isOwner,
options.showMyAppointmentsWeekChart,
);
for (let index = 0; index < Math.min(chartCount, 4); index += 1) {
cells.push({
id: `chart-skeleton-${index}`,
@@ -250,8 +265,6 @@ function buildDashboardCells(options: {
currentOrganization: ReturnType<typeof useAuth>['currentOrganization'];
}): TodayDashboardCell[] {
const cells: TodayDashboardCell[] = [];
const formatDayLabel = (code: string) =>
formatTodayChartDayLabel(code, options.dayLabelFormatter);
if (options.showCharts) {
cells.push(
@@ -260,7 +273,10 @@ function buildDashboardCells(options: {
charts: options.charts,
orgType: options.orgType,
isOwner: options.isOwner,
formatDayLabel,
showMyAppointmentsWeekChart: Boolean(
options.currentOrganization &&
canViewMyAppointmentsWeekChart(options.currentOrganization),
),
dayLabelFormatter: options.dayLabelFormatter,
}),
);
@@ -328,13 +344,13 @@ function buildChartCells(options: {
charts: TodaySummaryCharts;
orgType?: 'CLINIC' | 'LAB';
isOwner: boolean;
formatDayLabel: (code: string) => string;
showMyAppointmentsWeekChart: boolean;
dayLabelFormatter: ReturnType<typeof useTodayDayLabelFormatter>;
}): TodayDashboardCell[] {
const { t, charts, orgType, isOwner } = options;
const { t, charts, orgType, isOwner, showMyAppointmentsWeekChart } = options;
const cells: TodayDashboardCell[] = [];
const tallChart = TODAY_DASHBOARD_LAYOUT.chart;
const mediumChart = TODAY_DASHBOARD_LAYOUT.chartMedium;
const areaChart = TODAY_DASHBOARD_LAYOUT.chartArea;
const barChart = TODAY_DASHBOARD_LAYOUT.chartBar;
const appointmentsWeekAllData = mapWeekChartBuckets(
charts.appointmentsWeekAll ?? [],
@@ -348,11 +364,12 @@ function buildChartCells(options: {
charts.labTaskActivityWeek ?? [],
options.dayLabelFormatter,
);
const labTaskActivityChartData = mapLabTaskActivityChartData(labTaskActivityData);
if (orgType === 'CLINIC' && charts.appointmentsWeekAll !== undefined) {
cells.push({
id: 'chart-appointments-week-all',
layout: tallChart,
layout: areaChart,
content: (
<ChartCard
title={t('chartAppointmentsWeekAllTitle')}
@@ -366,10 +383,14 @@ function buildChartCells(options: {
});
}
if (orgType === 'CLINIC' && charts.appointmentsWeekMine !== undefined) {
if (
orgType === 'CLINIC' &&
showMyAppointmentsWeekChart &&
charts.appointmentsWeekMine !== undefined
) {
cells.push({
id: 'chart-appointments-week-mine',
layout: tallChart,
layout: areaChart,
content: (
<ChartCard
title={t('chartAppointmentsWeekMineTitle')}
@@ -386,7 +407,7 @@ function buildChartCells(options: {
if (orgType === 'LAB' && charts.labTaskActivityWeek !== undefined) {
cells.push({
id: 'chart-lab-task-activity',
layout: tallChart,
layout: areaChart,
content: (
<ChartCard
title={t('chartLabTaskActivityTitle')}
@@ -396,38 +417,10 @@ function buildChartCells(options: {
)}
emptyMessage={t('chartEmpty')}
>
<TodayStackedBarChart
data={labTaskActivityData}
<TodayLabTaskActivityChart
data={labTaskActivityChartData}
completedLabel={t('chartLabTaskCompletedLegend')}
receivedLabel={t('chartLabTaskReceivedLegend')}
formatDayLabel={options.formatDayLabel}
/>
</ChartCard>
),
});
}
const prosthesisData = charts.inProgressTasksByProsthesis ?? [];
if (orgType === 'LAB' && charts.inProgressTasksByProsthesis !== undefined) {
cells.push({
id: 'chart-prosthesis-mix',
layout: tallChart,
content: (
<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>
),
@@ -442,7 +435,7 @@ function buildChartCells(options: {
) {
cells.push({
id: 'chart-efficiency-report',
layout: tallChart,
layout: areaChart,
content: (
<ChartCard
title={t('chartEfficiencyReportTitle')}
@@ -453,14 +446,22 @@ function buildChartCells(options: {
}
isEmpty={efficiencyReportData.every((row) => row.count === 0)}
emptyMessage={t('chartEmpty')}
sidePanelLayout
chartPanel={
<TodayDonutChart
data={efficiencyReportData}
labelForCode={(code) =>
efficiencyReportData.find((row) => row.code === code)?.label ?? code
}
variant="pie"
/>
}
>
<TodayDonutChart
<TodayDonutChartLegend
data={efficiencyReportData}
labelForCode={(code) =>
efficiencyReportData.find((row) => row.code === code)?.label ?? code
}
variant="pie"
sideLegend
/>
</ChartCard>
),
@@ -471,7 +472,7 @@ function buildChartCells(options: {
if (orgType === 'CLINIC' && charts.appointmentsByProvider !== undefined) {
cells.push({
id: 'chart-appointments-by-provider',
layout: mediumChart,
layout: barChart,
content: (
<ChartCard
title={t('chartAppointmentsByProviderTitle')}
@@ -489,7 +490,7 @@ function buildChartCells(options: {
if (orgType === 'CLINIC' && charts.treatmentMixWeek !== undefined) {
cells.push({
id: 'chart-treatment-mix',
layout: mediumChart,
layout: barChart,
content: (
<ChartCard
title={t('chartTreatmentMixTitle')}
@@ -506,19 +507,22 @@ function buildChartCells(options: {
});
}
const tasksData = charts.tasksByWorkflowStep ?? [];
if (orgType === 'LAB' && charts.tasksByWorkflowStep !== undefined) {
const tasksByProsthesisData = charts.tasksByProsthesis ?? [];
if (orgType === 'LAB' && charts.tasksByProsthesis !== undefined) {
cells.push({
id: 'chart-tasks-by-step',
layout: mediumChart,
id: 'chart-tasks-by-prosthesis',
layout: barChart,
content: (
<ChartCard
title={t('chartTasksByStepTitle')}
subtitle={t('chartTasksByStepSubtitle')}
isEmpty={tasksData.length === 0}
title={t('chartTasksByProsthesisTitle')}
subtitle={t('chartTasksByProsthesisSubtitle')}
isEmpty={tasksByProsthesisData.length === 0}
emptyMessage={t('chartEmpty')}
>
<TodayBarChart data={tasksData} />
<TodayBarChart
data={tasksByProsthesisData}
colorForCode={(code, index) => prosthesisTypeColor(code, index)}
/>
</ChartCard>
),
});
@@ -531,18 +535,19 @@ function countVisibleCharts(
charts: TodaySummaryCharts,
orgType?: 'CLINIC' | 'LAB',
isOwner = false,
showMyAppointmentsWeekChart = false,
): number {
let count = 0;
if (orgType === 'CLINIC') {
count += charts.appointmentsWeekAll !== undefined ? 1 : 0;
count += charts.appointmentsWeekMine !== undefined ? 1 : 0;
count +=
showMyAppointmentsWeekChart && charts.appointmentsWeekMine !== undefined ? 1 : 0;
count += charts.appointmentsByProvider !== undefined ? 1 : 0;
count += charts.treatmentMixWeek !== undefined ? 1 : 0;
}
if (orgType === 'LAB') {
count += charts.labTaskActivityWeek !== undefined ? 1 : 0;
count += charts.inProgressTasksByProsthesis !== undefined ? 1 : 0;
count += charts.tasksByWorkflowStep !== undefined ? 1 : 0;
count += charts.tasksByProsthesis !== undefined ? 1 : 0;
}
if (
isOwner &&