Some functionality added to dashboard gadgets.
This commit is contained in:
@@ -38,13 +38,12 @@ type CaseCompletionChart = {
|
|||||||
|
|
||||||
type TodayCharts = {
|
type TodayCharts = {
|
||||||
treatmentMixWeek?: ChartBucket[];
|
treatmentMixWeek?: ChartBucket[];
|
||||||
tasksByWorkflowStep?: ChartBucket[];
|
tasksByProsthesis?: ChartBucket[];
|
||||||
appointmentsByProvider?: ChartBucket[];
|
appointmentsByProvider?: ChartBucket[];
|
||||||
caseCompletion?: CaseCompletionChart;
|
caseCompletion?: CaseCompletionChart;
|
||||||
appointmentsWeekAll?: ChartBucket[];
|
appointmentsWeekAll?: ChartBucket[];
|
||||||
appointmentsWeekMine?: ChartBucket[];
|
appointmentsWeekMine?: ChartBucket[];
|
||||||
labTaskActivityWeek?: StackedDayBucket[];
|
labTaskActivityWeek?: StackedDayBucket[];
|
||||||
inProgressTasksByProsthesis?: ChartBucket[];
|
|
||||||
efficiencyReport?: ChartBucket[];
|
efficiencyReport?: ChartBucket[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,7 +75,6 @@ type TodayWidgets = {
|
|||||||
appointmentsToday?: { count: number };
|
appointmentsToday?: { count: number };
|
||||||
patientsToday?: { count: number };
|
patientsToday?: { count: number };
|
||||||
treatmentsToday?: { count: number };
|
treatmentsToday?: { count: number };
|
||||||
draftTreatments?: { count: number };
|
|
||||||
labCasesPendingSend?: { count: number };
|
labCasesPendingSend?: { count: number };
|
||||||
casesReceivedToday?: { count: number };
|
casesReceivedToday?: { count: number };
|
||||||
casesInProgress?: { count: number };
|
casesInProgress?: { count: number };
|
||||||
@@ -144,11 +142,22 @@ export class TodayService {
|
|||||||
tasks.push(
|
tasks.push(
|
||||||
this.loadTreatmentsToday(organizationId, from, to, widgets),
|
this.loadTreatmentsToday(organizationId, from, to, widgets),
|
||||||
);
|
);
|
||||||
tasks.push(this.loadDraftTreatments(organizationId, widgets));
|
|
||||||
tasks.push(this.loadLabCasesPendingSend(organizationId, widgets));
|
tasks.push(this.loadLabCasesPendingSend(organizationId, widgets));
|
||||||
tasks.push(
|
tasks.push(
|
||||||
this.loadTreatmentMixWeek(organizationId, to, locale, charts),
|
this.loadTreatmentMixWeek(organizationId, to, locale, charts),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canViewMyAppointmentsWeekChart(membership.isOwner, permissionNames)) {
|
||||||
|
tasks.push(
|
||||||
|
this.loadUpcomingAppointmentsToday(
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
actions,
|
||||||
|
),
|
||||||
|
);
|
||||||
tasks.push(
|
tasks.push(
|
||||||
this.loadAppointmentsWeekMine(
|
this.loadAppointmentsWeekMine(
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -158,15 +167,6 @@ export class TodayService {
|
|||||||
charts,
|
charts,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
tasks.push(
|
|
||||||
this.loadUpcomingAppointmentsToday(
|
|
||||||
organizationId,
|
|
||||||
userId,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
actions,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -201,7 +201,7 @@ export class TodayService {
|
|||||||
if (this.canViewTasks(membership.isOwner, permissionNames)) {
|
if (this.canViewTasks(membership.isOwner, permissionNames)) {
|
||||||
tasks.push(this.loadTasksInProgress(organizationId, widgets));
|
tasks.push(this.loadTasksInProgress(organizationId, widgets));
|
||||||
tasks.push(this.loadImportantTasks(organizationId, widgets));
|
tasks.push(this.loadImportantTasks(organizationId, widgets));
|
||||||
tasks.push(this.loadTasksByWorkflowStep(organizationId, charts));
|
tasks.push(this.loadTasksByProsthesis(organizationId, locale, charts));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canViewLabWork) {
|
if (canViewLabWork) {
|
||||||
@@ -213,9 +213,6 @@ export class TodayService {
|
|||||||
charts,
|
charts,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
tasks.push(
|
|
||||||
this.loadInProgressTasksByProsthesis(organizationId, locale, charts),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,12 +302,13 @@ export class TodayService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadTasksByWorkflowStep(
|
private async loadTasksByProsthesis(
|
||||||
labOrganizationId: string,
|
labOrganizationId: string,
|
||||||
|
locale: CatalogLocale,
|
||||||
charts: TodayCharts,
|
charts: TodayCharts,
|
||||||
) {
|
) {
|
||||||
const grouped = await this.prisma.labCaseTask.groupBy({
|
const grouped = await this.prisma.labCaseTask.groupBy({
|
||||||
by: ['workflowStepCode', 'stepLabel'],
|
by: ['prosthesisTypeCode'],
|
||||||
where: {
|
where: {
|
||||||
status: LabTaskStatus.IN_PROGRESS,
|
status: LabTaskStatus.IN_PROGRESS,
|
||||||
labCase: {
|
labCase: {
|
||||||
@@ -321,14 +319,29 @@ export class TodayService {
|
|||||||
_count: { _all: true },
|
_count: { _all: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
charts.tasksByWorkflowStep = grouped
|
const sorted = grouped
|
||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
code: row.workflowStepCode,
|
code: row.prosthesisTypeCode,
|
||||||
label: row.stepLabel,
|
|
||||||
count: aggregateCount(row._count),
|
count: aggregateCount(row._count),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => b.count - a.count)
|
.sort((a, b) => b.count - a.count);
|
||||||
.slice(0, 10);
|
|
||||||
|
if (sorted.length === 0) {
|
||||||
|
charts.tasksByProsthesis = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = await this.catalogLabels.resolveLabels(
|
||||||
|
CatalogEntityKind.PROSTHESIS_TYPE,
|
||||||
|
sorted.map((row) => row.code),
|
||||||
|
locale,
|
||||||
|
);
|
||||||
|
|
||||||
|
charts.tasksByProsthesis = sorted.map((row) => ({
|
||||||
|
code: row.code,
|
||||||
|
label: labels.get(row.code) ?? row.code,
|
||||||
|
count: row.count,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveDayRange(query: TodaySummaryQueryDto): { from: Date; to: Date } {
|
private resolveDayRange(query: TodaySummaryQueryDto): { from: Date; to: Date } {
|
||||||
@@ -432,16 +445,6 @@ export class TodayService {
|
|||||||
widgets.treatmentsToday = { count };
|
widgets.treatmentsToday = { count };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadDraftTreatments(organizationId: string, widgets: TodayWidgets) {
|
|
||||||
const count = await this.prisma.treatment.count({
|
|
||||||
where: {
|
|
||||||
organizationId,
|
|
||||||
details: { none: {} },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
widgets.draftTreatments = { count };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadLabCasesPendingSend(organizationId: string, widgets: TodayWidgets) {
|
private async loadLabCasesPendingSend(organizationId: string, widgets: TodayWidgets) {
|
||||||
const count = await this.prisma.labCase.count({
|
const count = await this.prisma.labCase.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -983,48 +986,6 @@ export class TodayService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadInProgressTasksByProsthesis(
|
|
||||||
labOrganizationId: string,
|
|
||||||
locale: CatalogLocale,
|
|
||||||
charts: TodayCharts,
|
|
||||||
) {
|
|
||||||
const grouped = await this.prisma.labCaseTask.groupBy({
|
|
||||||
by: ['prosthesisTypeCode'],
|
|
||||||
where: {
|
|
||||||
status: LabTaskStatus.IN_PROGRESS,
|
|
||||||
labCase: {
|
|
||||||
sentAt: { not: null },
|
|
||||||
sends: { some: { organizationId: labOrganizationId } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_count: { _all: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const sorted = grouped
|
|
||||||
.map((row) => ({
|
|
||||||
code: row.prosthesisTypeCode,
|
|
||||||
count: aggregateCount(row._count),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.count - a.count);
|
|
||||||
|
|
||||||
if (sorted.length === 0) {
|
|
||||||
charts.inProgressTasksByProsthesis = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = await this.catalogLabels.resolveLabels(
|
|
||||||
CatalogEntityKind.PROSTHESIS_TYPE,
|
|
||||||
sorted.map((row) => row.code),
|
|
||||||
locale,
|
|
||||||
);
|
|
||||||
|
|
||||||
charts.inProgressTasksByProsthesis = sorted.map((row) => ({
|
|
||||||
code: row.code,
|
|
||||||
label: labels.get(row.code) ?? row.code,
|
|
||||||
count: row.count,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRequesterOrganizationId(sharedDataTypes: unknown): string | null {
|
private getRequesterOrganizationId(sharedDataTypes: unknown): string | null {
|
||||||
if (!sharedDataTypes || typeof sharedDataTypes !== 'object') {
|
if (!sharedDataTypes || typeof sharedDataTypes !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
@@ -1106,6 +1067,11 @@ export class TodayService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private canViewMyAppointmentsWeekChart(isOwner: boolean, names: string[]): boolean {
|
||||||
|
if (isOwner) return false;
|
||||||
|
return names.includes('TAB_TREATMENT_EDIT');
|
||||||
|
}
|
||||||
|
|
||||||
private canViewCases(isOwner: boolean, names: string[]): boolean {
|
private canViewCases(isOwner: boolean, names: string[]): boolean {
|
||||||
if (isOwner) return true;
|
if (isOwner) return true;
|
||||||
return names.some((p) =>
|
return names.some((p) =>
|
||||||
|
|||||||
@@ -227,8 +227,6 @@
|
|||||||
"chartLabTaskActivitySubtitle": "Last 7 days",
|
"chartLabTaskActivitySubtitle": "Last 7 days",
|
||||||
"chartLabTaskCompletedLegend": "Completed",
|
"chartLabTaskCompletedLegend": "Completed",
|
||||||
"chartLabTaskReceivedLegend": "Received",
|
"chartLabTaskReceivedLegend": "Received",
|
||||||
"chartProsthesisMixTitle": "In-Progress Tasks by Prosthesis",
|
|
||||||
"chartProsthesisMixSubtitle": "Current workload mix",
|
|
||||||
"chartAppointmentsByProviderTitle": "Appointments by Provider",
|
"chartAppointmentsByProviderTitle": "Appointments by Provider",
|
||||||
"chartAppointmentsByProviderSubtitle": "Today",
|
"chartAppointmentsByProviderSubtitle": "Today",
|
||||||
"chartTreatmentMixTitle": "Treatment Mix",
|
"chartTreatmentMixTitle": "Treatment Mix",
|
||||||
@@ -237,8 +235,8 @@
|
|||||||
"chartCaseCompletionSubtitle": "All active cases",
|
"chartCaseCompletionSubtitle": "All active cases",
|
||||||
"chartCaseCompletionPercent": "{percent}%",
|
"chartCaseCompletionPercent": "{percent}%",
|
||||||
"chartCaseCompletionTasks": "Tasks completed",
|
"chartCaseCompletionTasks": "Tasks completed",
|
||||||
"chartTasksByStepTitle": "Tasks by Workflow Step",
|
"chartTasksByProsthesisTitle": "In-Progress Tasks by Prosthesis",
|
||||||
"chartTasksByStepSubtitle": "In progress now",
|
"chartTasksByProsthesisSubtitle": "Current workload mix",
|
||||||
"chartEfficiencyReportTitle": "Efficiency Report",
|
"chartEfficiencyReportTitle": "Efficiency Report",
|
||||||
"chartEfficiencyReportSubtitleClinic": "Treatments created by staff — last 30 days",
|
"chartEfficiencyReportSubtitleClinic": "Treatments created by staff — last 30 days",
|
||||||
"chartEfficiencyReportSubtitleLab": "Tasks completed by staff — last 30 days",
|
"chartEfficiencyReportSubtitleLab": "Tasks completed by staff — last 30 days",
|
||||||
|
|||||||
@@ -227,8 +227,6 @@
|
|||||||
"chartLabTaskActivitySubtitle": "۷ روز گذشته",
|
"chartLabTaskActivitySubtitle": "۷ روز گذشته",
|
||||||
"chartLabTaskCompletedLegend": "تکمیلشده",
|
"chartLabTaskCompletedLegend": "تکمیلشده",
|
||||||
"chartLabTaskReceivedLegend": "دریافتشده",
|
"chartLabTaskReceivedLegend": "دریافتشده",
|
||||||
"chartProsthesisMixTitle": "وظایف در حال انجام بر اساس پروتز",
|
|
||||||
"chartProsthesisMixSubtitle": "ترکیب بار کاری فعلی",
|
|
||||||
"chartAppointmentsByProviderTitle": "نوبتها بر اساس ارائهدهنده",
|
"chartAppointmentsByProviderTitle": "نوبتها بر اساس ارائهدهنده",
|
||||||
"chartAppointmentsByProviderSubtitle": "امروز",
|
"chartAppointmentsByProviderSubtitle": "امروز",
|
||||||
"chartTreatmentMixTitle": "ترکیب درمانها",
|
"chartTreatmentMixTitle": "ترکیب درمانها",
|
||||||
@@ -237,8 +235,8 @@
|
|||||||
"chartCaseCompletionSubtitle": "همه پروندههای فعال",
|
"chartCaseCompletionSubtitle": "همه پروندههای فعال",
|
||||||
"chartCaseCompletionPercent": "{percent}٪",
|
"chartCaseCompletionPercent": "{percent}٪",
|
||||||
"chartCaseCompletionTasks": "وظایف تکمیلشده",
|
"chartCaseCompletionTasks": "وظایف تکمیلشده",
|
||||||
"chartTasksByStepTitle": "وظایف بر اساس مرحله گردش کار",
|
"chartTasksByProsthesisTitle": "وظایف در حال انجام بر اساس پروتز",
|
||||||
"chartTasksByStepSubtitle": "در حال انجام",
|
"chartTasksByProsthesisSubtitle": "ترکیب بار کاری فعلی",
|
||||||
"chartEfficiencyReportTitle": "گزارش کارایی",
|
"chartEfficiencyReportTitle": "گزارش کارایی",
|
||||||
"chartEfficiencyReportSubtitleClinic": "درمانهای ثبتشده توسط کارکنان — ۳۰ روز گذشته",
|
"chartEfficiencyReportSubtitleClinic": "درمانهای ثبتشده توسط کارکنان — ۳۰ روز گذشته",
|
||||||
"chartEfficiencyReportSubtitleLab": "وظایف تکمیلشده توسط کارکنان — ۳۰ روز گذشته",
|
"chartEfficiencyReportSubtitleLab": "وظایف تکمیلشده توسط کارکنان — ۳۰ روز گذشته",
|
||||||
|
|||||||
@@ -227,8 +227,6 @@
|
|||||||
"chartLabTaskActivitySubtitle": "Afgelopen 7 dagen",
|
"chartLabTaskActivitySubtitle": "Afgelopen 7 dagen",
|
||||||
"chartLabTaskCompletedLegend": "Voltooid",
|
"chartLabTaskCompletedLegend": "Voltooid",
|
||||||
"chartLabTaskReceivedLegend": "Ontvangen",
|
"chartLabTaskReceivedLegend": "Ontvangen",
|
||||||
"chartProsthesisMixTitle": "Lopende taken per prothese",
|
|
||||||
"chartProsthesisMixSubtitle": "Huidige werklastmix",
|
|
||||||
"chartAppointmentsByProviderTitle": "Afspraken per behandelaar",
|
"chartAppointmentsByProviderTitle": "Afspraken per behandelaar",
|
||||||
"chartAppointmentsByProviderSubtitle": "Vandaag",
|
"chartAppointmentsByProviderSubtitle": "Vandaag",
|
||||||
"chartTreatmentMixTitle": "Behandelingsmix",
|
"chartTreatmentMixTitle": "Behandelingsmix",
|
||||||
@@ -237,8 +235,8 @@
|
|||||||
"chartCaseCompletionSubtitle": "Alle actieve cases",
|
"chartCaseCompletionSubtitle": "Alle actieve cases",
|
||||||
"chartCaseCompletionPercent": "{percent}%",
|
"chartCaseCompletionPercent": "{percent}%",
|
||||||
"chartCaseCompletionTasks": "Taken voltooid",
|
"chartCaseCompletionTasks": "Taken voltooid",
|
||||||
"chartTasksByStepTitle": "Taken per workflowstap",
|
"chartTasksByProsthesisTitle": "Lopende taken per prothese",
|
||||||
"chartTasksByStepSubtitle": "Nu in uitvoering",
|
"chartTasksByProsthesisSubtitle": "Huidige werklastmix",
|
||||||
"chartEfficiencyReportTitle": "Efficiëntierapport",
|
"chartEfficiencyReportTitle": "Efficiëntierapport",
|
||||||
"chartEfficiencyReportSubtitleClinic": "Behandelingen aangemaakt door medewerkers — afgelopen 30 dagen",
|
"chartEfficiencyReportSubtitleClinic": "Behandelingen aangemaakt door medewerkers — afgelopen 30 dagen",
|
||||||
"chartEfficiencyReportSubtitleLab": "Taken voltooid door medewerkers — afgelopen 30 dagen",
|
"chartEfficiencyReportSubtitleLab": "Taken voltooid door medewerkers — afgelopen 30 dagen",
|
||||||
|
|||||||
@@ -146,6 +146,14 @@ export function canEditTreatment(org: Organization | null): boolean {
|
|||||||
return hasPermission(org, 'TAB_TREATMENT_EDIT');
|
return hasPermission(org, 'TAB_TREATMENT_EDIT');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Staff treatment editors only — personal schedule Today gadgets (not owners). */
|
||||||
|
export function canViewMyAppointmentsWeekChart(org: Organization | null): boolean {
|
||||||
|
if (!org) return false;
|
||||||
|
if (org.type !== 'CLINIC') return false;
|
||||||
|
if (org.isOwner) return false;
|
||||||
|
return hasPermission(org, 'TAB_TREATMENT_EDIT');
|
||||||
|
}
|
||||||
|
|
||||||
/** View treatment workspace (read-only or edit) */
|
/** View treatment workspace (read-only or edit) */
|
||||||
export function canViewTreatment(org: Organization | null): boolean {
|
export function canViewTreatment(org: Organization | null): boolean {
|
||||||
if (!org) return false;
|
if (!org) return false;
|
||||||
|
|||||||
@@ -9,6 +9,24 @@ interface ChartCardProps {
|
|||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
isEmpty?: boolean;
|
isEmpty?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
/**
|
||||||
|
* Two-column layout: left 2/3 (header + children), right 1/3 (chartPanel).
|
||||||
|
* Chart column is independent and vertically centered.
|
||||||
|
*/
|
||||||
|
sidePanelLayout?: boolean;
|
||||||
|
chartPanel?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartCardHeader({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
}: Pick<ChartCardProps, 'title' | 'subtitle'>) {
|
||||||
|
return (
|
||||||
|
<div className="mb-3 shrink-0">
|
||||||
|
<h2 className="text-base font-semibold text-card-foreground">{title}</h2>
|
||||||
|
{subtitle ? <p className="mt-1 text-xs text-text-muted">{subtitle}</p> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChartCard({
|
export function ChartCard({
|
||||||
@@ -18,23 +36,47 @@ export function ChartCard({
|
|||||||
emptyMessage,
|
emptyMessage,
|
||||||
isEmpty = false,
|
isEmpty = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
sidePanelLayout = false,
|
||||||
|
chartPanel,
|
||||||
}: ChartCardProps) {
|
}: ChartCardProps) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ChartCardSkeleton />;
|
return <ChartCardSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sidePanelLayout) {
|
||||||
|
return (
|
||||||
|
<Card className="grid h-full min-h-0 grid-cols-[2fr_1fr] gap-x-3 overflow-hidden">
|
||||||
|
<div className="flex min-h-0 flex-col overflow-hidden">
|
||||||
|
<ChartCardHeader 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">
|
||||||
|
<p className="px-4 text-center text-sm text-text-muted">{emptyMessage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="min-h-0 flex-1 overflow-hidden">{children}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!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}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex h-full min-h-0 flex-col overflow-hidden">
|
<Card className="flex h-full min-h-0 flex-col overflow-hidden">
|
||||||
<div className="mb-4 shrink-0">
|
<ChartCardHeader title={title} subtitle={subtitle} />
|
||||||
<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>
|
|
||||||
|
|
||||||
{isEmpty ? (
|
{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">
|
<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">
|
||||||
<p className="text-sm text-text-muted text-center px-4">{emptyMessage}</p>
|
<p className="px-4 text-center text-sm text-text-muted">{emptyMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
|
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
|
||||||
|
|||||||
@@ -20,27 +20,39 @@ import {
|
|||||||
|
|
||||||
interface TodayAreaChartProps {
|
interface TodayAreaChartProps {
|
||||||
data: TodayChartBucket[];
|
data: TodayChartBucket[];
|
||||||
|
color?: string;
|
||||||
|
gradientId?: string;
|
||||||
|
showXAxis?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TodayAreaChart({ data }: TodayAreaChartProps) {
|
export function TodayAreaChart({
|
||||||
|
data,
|
||||||
|
color = TODAY_CHART_PRIMARY_COLOR,
|
||||||
|
gradientId = 'todayAreaFill',
|
||||||
|
showXAxis = true,
|
||||||
|
}: TodayAreaChartProps) {
|
||||||
return (
|
return (
|
||||||
<TodayChartFrame>
|
<TodayChartFrame>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<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: showXAxis ? 0 : -4 }}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="todayAreaFill" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stopColor={TODAY_CHART_PRIMARY_COLOR} stopOpacity={0.45} />
|
<stop offset="0%" stopColor={color} stopOpacity={0.45} />
|
||||||
<stop offset="100%" stopColor={TODAY_CHART_PRIMARY_COLOR} stopOpacity={0.05} />
|
<stop offset="100%" stopColor={color} stopOpacity={0.05} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid stroke={TODAY_CHART_GRID_COLOR} vertical={false} />
|
<CartesianGrid stroke={TODAY_CHART_GRID_COLOR} vertical={false} />
|
||||||
<XAxis
|
{showXAxis ? (
|
||||||
dataKey="label"
|
<XAxis
|
||||||
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
|
dataKey="label"
|
||||||
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
|
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
|
||||||
tickLine={false}
|
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
|
||||||
interval={1}
|
tickLine={false}
|
||||||
/>
|
interval={1}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<XAxis dataKey="label" hide />
|
||||||
|
)}
|
||||||
<YAxis
|
<YAxis
|
||||||
allowDecimals={false}
|
allowDecimals={false}
|
||||||
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
|
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
|
||||||
@@ -56,11 +68,11 @@ export function TodayAreaChart({ data }: TodayAreaChartProps) {
|
|||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="count"
|
dataKey="count"
|
||||||
stroke={TODAY_CHART_PRIMARY_COLOR}
|
stroke={color}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
fill="url(#todayAreaFill)"
|
fill={`url(#${gradientId})`}
|
||||||
dot={{ r: 3, fill: TODAY_CHART_PRIMARY_COLOR, strokeWidth: 0 }}
|
dot={{ r: 3, fill: color, strokeWidth: 0 }}
|
||||||
activeDot={{ r: 5, fill: TODAY_CHART_PRIMARY_COLOR }}
|
activeDot={{ r: 5, fill: color }}
|
||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
|||||||
@@ -40,9 +40,12 @@ export function TodayCaseCompletionKpiCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex min-h-0 flex-1 items-center justify-center">
|
<div className="mt-2 flex min-h-0 flex-1 items-center justify-center">
|
||||||
<div className="w-1/2 min-w-0">
|
<div className="w-[58%] min-w-0">
|
||||||
<TodayRadialGaugeChart
|
<TodayRadialGaugeChart
|
||||||
size="sm"
|
size="sm"
|
||||||
|
compactClassName="h-[120px]"
|
||||||
|
innerRadius="72%"
|
||||||
|
compactBarSize={8}
|
||||||
percent={total > 0 ? percent : 0}
|
percent={total > 0 ? percent : 0}
|
||||||
completed={completed}
|
completed={completed}
|
||||||
total={total}
|
total={total}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
canViewAppointmentsTab,
|
canViewAppointmentsTab,
|
||||||
canViewCases,
|
canViewCases,
|
||||||
canViewLabCasesOrTasks,
|
canViewLabCasesOrTasks,
|
||||||
|
canViewMyAppointmentsWeekChart,
|
||||||
canViewTasks,
|
canViewTasks,
|
||||||
canViewTreatment,
|
canViewTreatment,
|
||||||
} from '@/components/shared/permissions';
|
} from '@/components/shared/permissions';
|
||||||
@@ -15,15 +16,17 @@ import { ChartCard } from '@/components/today/ChartCard';
|
|||||||
import { TodayAreaChart } from '@/components/today/TodayAreaChart';
|
import { TodayAreaChart } from '@/components/today/TodayAreaChart';
|
||||||
import { TodayBarChart } from '@/components/today/TodayBarChart';
|
import { TodayBarChart } from '@/components/today/TodayBarChart';
|
||||||
import {
|
import {
|
||||||
formatTodayChartDayLabel,
|
|
||||||
mapWeekChartBuckets,
|
mapWeekChartBuckets,
|
||||||
useTodayDayLabelFormatter,
|
useTodayDayLabelFormatter,
|
||||||
} from '@/components/today/chart-day-labels';
|
} from '@/components/today/chart-day-labels';
|
||||||
import { TodayDashboardGrid } from '@/components/today/TodayDashboardGrid';
|
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 { TodayHorizontalBarChart } from '@/components/today/TodayHorizontalBarChart';
|
||||||
import { TodayCaseCompletionKpiCard } from '@/components/today/TodayCaseCompletionKpiCard';
|
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 { TodaySubscriptionKpiCard } from '@/components/today/TodaySubscriptionKpiCard';
|
||||||
import { TodayUpcomingAppointments } from '@/components/today/TodayUpcomingAppointments';
|
import { TodayUpcomingAppointments } from '@/components/today/TodayUpcomingAppointments';
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +39,7 @@ import {
|
|||||||
type TodayDashboardCell,
|
type TodayDashboardCell,
|
||||||
} from '@/components/today/today-dashboard-layout';
|
} from '@/components/today/today-dashboard-layout';
|
||||||
import { getEligibleTodayKpis, getVisibleTodayKpis } from '@/components/today/widget-registry';
|
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 { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay';
|
||||||
import type {
|
import type {
|
||||||
TodaySubscriptionSnapshot,
|
TodaySubscriptionSnapshot,
|
||||||
@@ -71,7 +74,9 @@ export function TodayDashboard({
|
|||||||
const isOwner = Boolean(currentOrganization?.isOwner);
|
const isOwner = Boolean(currentOrganization?.isOwner);
|
||||||
|
|
||||||
const showUpcoming =
|
const showUpcoming =
|
||||||
orgType === 'CLINIC' && currentOrganization && canViewTreatment(currentOrganization);
|
orgType === 'CLINIC' &&
|
||||||
|
currentOrganization &&
|
||||||
|
canViewMyAppointmentsWeekChart(currentOrganization);
|
||||||
|
|
||||||
const showCharts = useMemo(() => {
|
const showCharts = useMemo(() => {
|
||||||
if (!orgType || !currentOrganization) return false;
|
if (!orgType || !currentOrganization) return false;
|
||||||
@@ -109,6 +114,10 @@ export function TodayDashboard({
|
|||||||
showCharts,
|
showCharts,
|
||||||
orgType,
|
orgType,
|
||||||
isOwner,
|
isOwner,
|
||||||
|
showMyAppointmentsWeekChart: Boolean(
|
||||||
|
currentOrganization &&
|
||||||
|
canViewMyAppointmentsWeekChart(currentOrganization),
|
||||||
|
),
|
||||||
charts,
|
charts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -171,12 +180,18 @@ function buildSkeletonCells(options: {
|
|||||||
showCharts: boolean;
|
showCharts: boolean;
|
||||||
orgType?: 'CLINIC' | 'LAB';
|
orgType?: 'CLINIC' | 'LAB';
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
|
showMyAppointmentsWeekChart: boolean;
|
||||||
charts: TodaySummaryCharts;
|
charts: TodaySummaryCharts;
|
||||||
}): TodayDashboardCell[] {
|
}): TodayDashboardCell[] {
|
||||||
const cells: TodayDashboardCell[] = [];
|
const cells: TodayDashboardCell[] = [];
|
||||||
|
|
||||||
if (options.showCharts) {
|
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) {
|
for (let index = 0; index < Math.min(chartCount, 4); index += 1) {
|
||||||
cells.push({
|
cells.push({
|
||||||
id: `chart-skeleton-${index}`,
|
id: `chart-skeleton-${index}`,
|
||||||
@@ -250,8 +265,6 @@ function buildDashboardCells(options: {
|
|||||||
currentOrganization: ReturnType<typeof useAuth>['currentOrganization'];
|
currentOrganization: ReturnType<typeof useAuth>['currentOrganization'];
|
||||||
}): TodayDashboardCell[] {
|
}): TodayDashboardCell[] {
|
||||||
const cells: TodayDashboardCell[] = [];
|
const cells: TodayDashboardCell[] = [];
|
||||||
const formatDayLabel = (code: string) =>
|
|
||||||
formatTodayChartDayLabel(code, options.dayLabelFormatter);
|
|
||||||
|
|
||||||
if (options.showCharts) {
|
if (options.showCharts) {
|
||||||
cells.push(
|
cells.push(
|
||||||
@@ -260,7 +273,10 @@ function buildDashboardCells(options: {
|
|||||||
charts: options.charts,
|
charts: options.charts,
|
||||||
orgType: options.orgType,
|
orgType: options.orgType,
|
||||||
isOwner: options.isOwner,
|
isOwner: options.isOwner,
|
||||||
formatDayLabel,
|
showMyAppointmentsWeekChart: Boolean(
|
||||||
|
options.currentOrganization &&
|
||||||
|
canViewMyAppointmentsWeekChart(options.currentOrganization),
|
||||||
|
),
|
||||||
dayLabelFormatter: options.dayLabelFormatter,
|
dayLabelFormatter: options.dayLabelFormatter,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -328,13 +344,13 @@ function buildChartCells(options: {
|
|||||||
charts: TodaySummaryCharts;
|
charts: TodaySummaryCharts;
|
||||||
orgType?: 'CLINIC' | 'LAB';
|
orgType?: 'CLINIC' | 'LAB';
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
formatDayLabel: (code: string) => string;
|
showMyAppointmentsWeekChart: boolean;
|
||||||
dayLabelFormatter: ReturnType<typeof useTodayDayLabelFormatter>;
|
dayLabelFormatter: ReturnType<typeof useTodayDayLabelFormatter>;
|
||||||
}): TodayDashboardCell[] {
|
}): TodayDashboardCell[] {
|
||||||
const { t, charts, orgType, isOwner } = options;
|
const { t, charts, orgType, isOwner, showMyAppointmentsWeekChart } = options;
|
||||||
const cells: TodayDashboardCell[] = [];
|
const cells: TodayDashboardCell[] = [];
|
||||||
const tallChart = TODAY_DASHBOARD_LAYOUT.chart;
|
const areaChart = TODAY_DASHBOARD_LAYOUT.chartArea;
|
||||||
const mediumChart = TODAY_DASHBOARD_LAYOUT.chartMedium;
|
const barChart = TODAY_DASHBOARD_LAYOUT.chartBar;
|
||||||
|
|
||||||
const appointmentsWeekAllData = mapWeekChartBuckets(
|
const appointmentsWeekAllData = mapWeekChartBuckets(
|
||||||
charts.appointmentsWeekAll ?? [],
|
charts.appointmentsWeekAll ?? [],
|
||||||
@@ -348,11 +364,12 @@ function buildChartCells(options: {
|
|||||||
charts.labTaskActivityWeek ?? [],
|
charts.labTaskActivityWeek ?? [],
|
||||||
options.dayLabelFormatter,
|
options.dayLabelFormatter,
|
||||||
);
|
);
|
||||||
|
const labTaskActivityChartData = mapLabTaskActivityChartData(labTaskActivityData);
|
||||||
|
|
||||||
if (orgType === 'CLINIC' && charts.appointmentsWeekAll !== undefined) {
|
if (orgType === 'CLINIC' && charts.appointmentsWeekAll !== undefined) {
|
||||||
cells.push({
|
cells.push({
|
||||||
id: 'chart-appointments-week-all',
|
id: 'chart-appointments-week-all',
|
||||||
layout: tallChart,
|
layout: areaChart,
|
||||||
content: (
|
content: (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title={t('chartAppointmentsWeekAllTitle')}
|
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({
|
cells.push({
|
||||||
id: 'chart-appointments-week-mine',
|
id: 'chart-appointments-week-mine',
|
||||||
layout: tallChart,
|
layout: areaChart,
|
||||||
content: (
|
content: (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title={t('chartAppointmentsWeekMineTitle')}
|
title={t('chartAppointmentsWeekMineTitle')}
|
||||||
@@ -386,7 +407,7 @@ function buildChartCells(options: {
|
|||||||
if (orgType === 'LAB' && charts.labTaskActivityWeek !== undefined) {
|
if (orgType === 'LAB' && charts.labTaskActivityWeek !== undefined) {
|
||||||
cells.push({
|
cells.push({
|
||||||
id: 'chart-lab-task-activity',
|
id: 'chart-lab-task-activity',
|
||||||
layout: tallChart,
|
layout: areaChart,
|
||||||
content: (
|
content: (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title={t('chartLabTaskActivityTitle')}
|
title={t('chartLabTaskActivityTitle')}
|
||||||
@@ -396,38 +417,10 @@ function buildChartCells(options: {
|
|||||||
)}
|
)}
|
||||||
emptyMessage={t('chartEmpty')}
|
emptyMessage={t('chartEmpty')}
|
||||||
>
|
>
|
||||||
<TodayStackedBarChart
|
<TodayLabTaskActivityChart
|
||||||
data={labTaskActivityData}
|
data={labTaskActivityChartData}
|
||||||
completedLabel={t('chartLabTaskCompletedLegend')}
|
completedLabel={t('chartLabTaskCompletedLegend')}
|
||||||
receivedLabel={t('chartLabTaskReceivedLegend')}
|
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>
|
</ChartCard>
|
||||||
),
|
),
|
||||||
@@ -442,7 +435,7 @@ function buildChartCells(options: {
|
|||||||
) {
|
) {
|
||||||
cells.push({
|
cells.push({
|
||||||
id: 'chart-efficiency-report',
|
id: 'chart-efficiency-report',
|
||||||
layout: tallChart,
|
layout: areaChart,
|
||||||
content: (
|
content: (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title={t('chartEfficiencyReportTitle')}
|
title={t('chartEfficiencyReportTitle')}
|
||||||
@@ -453,14 +446,22 @@ function buildChartCells(options: {
|
|||||||
}
|
}
|
||||||
isEmpty={efficiencyReportData.every((row) => row.count === 0)}
|
isEmpty={efficiencyReportData.every((row) => row.count === 0)}
|
||||||
emptyMessage={t('chartEmpty')}
|
emptyMessage={t('chartEmpty')}
|
||||||
|
sidePanelLayout
|
||||||
|
chartPanel={
|
||||||
|
<TodayDonutChart
|
||||||
|
data={efficiencyReportData}
|
||||||
|
labelForCode={(code) =>
|
||||||
|
efficiencyReportData.find((row) => row.code === code)?.label ?? code
|
||||||
|
}
|
||||||
|
variant="pie"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TodayDonutChart
|
<TodayDonutChartLegend
|
||||||
data={efficiencyReportData}
|
data={efficiencyReportData}
|
||||||
labelForCode={(code) =>
|
labelForCode={(code) =>
|
||||||
efficiencyReportData.find((row) => row.code === code)?.label ?? code
|
efficiencyReportData.find((row) => row.code === code)?.label ?? code
|
||||||
}
|
}
|
||||||
variant="pie"
|
|
||||||
sideLegend
|
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
),
|
),
|
||||||
@@ -471,7 +472,7 @@ function buildChartCells(options: {
|
|||||||
if (orgType === 'CLINIC' && charts.appointmentsByProvider !== undefined) {
|
if (orgType === 'CLINIC' && charts.appointmentsByProvider !== undefined) {
|
||||||
cells.push({
|
cells.push({
|
||||||
id: 'chart-appointments-by-provider',
|
id: 'chart-appointments-by-provider',
|
||||||
layout: mediumChart,
|
layout: barChart,
|
||||||
content: (
|
content: (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title={t('chartAppointmentsByProviderTitle')}
|
title={t('chartAppointmentsByProviderTitle')}
|
||||||
@@ -489,7 +490,7 @@ function buildChartCells(options: {
|
|||||||
if (orgType === 'CLINIC' && charts.treatmentMixWeek !== undefined) {
|
if (orgType === 'CLINIC' && charts.treatmentMixWeek !== undefined) {
|
||||||
cells.push({
|
cells.push({
|
||||||
id: 'chart-treatment-mix',
|
id: 'chart-treatment-mix',
|
||||||
layout: mediumChart,
|
layout: barChart,
|
||||||
content: (
|
content: (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title={t('chartTreatmentMixTitle')}
|
title={t('chartTreatmentMixTitle')}
|
||||||
@@ -506,19 +507,22 @@ function buildChartCells(options: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasksData = charts.tasksByWorkflowStep ?? [];
|
const tasksByProsthesisData = charts.tasksByProsthesis ?? [];
|
||||||
if (orgType === 'LAB' && charts.tasksByWorkflowStep !== undefined) {
|
if (orgType === 'LAB' && charts.tasksByProsthesis !== undefined) {
|
||||||
cells.push({
|
cells.push({
|
||||||
id: 'chart-tasks-by-step',
|
id: 'chart-tasks-by-prosthesis',
|
||||||
layout: mediumChart,
|
layout: barChart,
|
||||||
content: (
|
content: (
|
||||||
<ChartCard
|
<ChartCard
|
||||||
title={t('chartTasksByStepTitle')}
|
title={t('chartTasksByProsthesisTitle')}
|
||||||
subtitle={t('chartTasksByStepSubtitle')}
|
subtitle={t('chartTasksByProsthesisSubtitle')}
|
||||||
isEmpty={tasksData.length === 0}
|
isEmpty={tasksByProsthesisData.length === 0}
|
||||||
emptyMessage={t('chartEmpty')}
|
emptyMessage={t('chartEmpty')}
|
||||||
>
|
>
|
||||||
<TodayBarChart data={tasksData} />
|
<TodayBarChart
|
||||||
|
data={tasksByProsthesisData}
|
||||||
|
colorForCode={(code, index) => prosthesisTypeColor(code, index)}
|
||||||
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -531,18 +535,19 @@ function countVisibleCharts(
|
|||||||
charts: TodaySummaryCharts,
|
charts: TodaySummaryCharts,
|
||||||
orgType?: 'CLINIC' | 'LAB',
|
orgType?: 'CLINIC' | 'LAB',
|
||||||
isOwner = false,
|
isOwner = false,
|
||||||
|
showMyAppointmentsWeekChart = false,
|
||||||
): number {
|
): number {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
if (orgType === 'CLINIC') {
|
if (orgType === 'CLINIC') {
|
||||||
count += charts.appointmentsWeekAll !== undefined ? 1 : 0;
|
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.appointmentsByProvider !== undefined ? 1 : 0;
|
||||||
count += charts.treatmentMixWeek !== undefined ? 1 : 0;
|
count += charts.treatmentMixWeek !== undefined ? 1 : 0;
|
||||||
}
|
}
|
||||||
if (orgType === 'LAB') {
|
if (orgType === 'LAB') {
|
||||||
count += charts.labTaskActivityWeek !== undefined ? 1 : 0;
|
count += charts.labTaskActivityWeek !== undefined ? 1 : 0;
|
||||||
count += charts.inProgressTasksByProsthesis !== undefined ? 1 : 0;
|
count += charts.tasksByProsthesis !== undefined ? 1 : 0;
|
||||||
count += charts.tasksByWorkflowStep !== undefined ? 1 : 0;
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
isOwner &&
|
isOwner &&
|
||||||
|
|||||||
@@ -5,19 +5,100 @@ import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
|||||||
import type { TodayChartBucket } from '@/types/today';
|
import type { TodayChartBucket } from '@/types/today';
|
||||||
import { TodayChartFrame } from '@/components/today/TodayChartFrame';
|
import { TodayChartFrame } from '@/components/today/TodayChartFrame';
|
||||||
import {
|
import {
|
||||||
TODAY_CHART_COLORS,
|
chartRankColor,
|
||||||
TODAY_CHART_TOOLTIP_STYLE,
|
TODAY_CHART_TOOLTIP_STYLE,
|
||||||
} from '@/components/today/chart-theme';
|
} from '@/components/today/chart-theme';
|
||||||
|
|
||||||
interface TodayDonutChartProps {
|
interface TodayDonutChartBaseProps {
|
||||||
data: TodayChartBucket[];
|
data: TodayChartBucket[];
|
||||||
labelForCode: (code: string) => string;
|
labelForCode: (code: string) => string;
|
||||||
colorForCode?: (code: string, index: number) => string;
|
colorForCode?: (code: string, index: number) => string;
|
||||||
swatchStyleForCode?: (code: string, index: number) => CSSProperties;
|
swatchStyleForCode?: (code: string, index: number) => CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TodayDonutChartProps extends TodayDonutChartBaseProps {
|
||||||
variant?: 'donut' | 'pie';
|
variant?: 'donut' | 'pie';
|
||||||
|
/** Inline legend + chart row (legacy). Prefer TodayDonutChartLegend + sidePanelLayout. */
|
||||||
sideLegend?: boolean;
|
sideLegend?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useDonutChartModel({
|
||||||
|
data,
|
||||||
|
labelForCode,
|
||||||
|
colorForCode,
|
||||||
|
swatchStyleForCode,
|
||||||
|
}: TodayDonutChartBaseProps) {
|
||||||
|
const chartData = data.map((item) => ({
|
||||||
|
...item,
|
||||||
|
displayLabel: labelForCode(item.code),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resolveColor = (code: string, index: number) =>
|
||||||
|
colorForCode?.(code, index) ?? chartRankColor(index);
|
||||||
|
|
||||||
|
const resolveSwatchStyle = (code: string, index: number): CSSProperties =>
|
||||||
|
swatchStyleForCode?.(code, index) ?? {
|
||||||
|
backgroundColor: resolveColor(code, index),
|
||||||
|
borderColor: 'rgba(0, 0, 0, 0.18)',
|
||||||
|
};
|
||||||
|
|
||||||
|
return { chartData, resolveColor, resolveSwatchStyle };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TodayDonutChartLegend({
|
||||||
|
data,
|
||||||
|
labelForCode,
|
||||||
|
colorForCode,
|
||||||
|
swatchStyleForCode,
|
||||||
|
}: TodayDonutChartBaseProps) {
|
||||||
|
const { chartData, resolveSwatchStyle } = useDonutChartModel({
|
||||||
|
data,
|
||||||
|
labelForCode,
|
||||||
|
colorForCode,
|
||||||
|
swatchStyleForCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rowClass = 'flex h-4 items-center text-xs leading-none';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-0 items-start overflow-hidden">
|
||||||
|
<div className="flex max-h-full min-w-0 flex-col items-start gap-1.5 overflow-y-auto">
|
||||||
|
{chartData.map((entry, index) => (
|
||||||
|
<span key={entry.code} className={rowClass}>
|
||||||
|
<span
|
||||||
|
className="inline-block h-3 w-3 rounded-sm border"
|
||||||
|
style={resolveSwatchStyle(entry.code, index)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-2 flex min-w-0 flex-col items-start gap-1.5 overflow-hidden">
|
||||||
|
{chartData.map((entry) => (
|
||||||
|
<span
|
||||||
|
key={entry.code}
|
||||||
|
className={`${rowClass} max-w-full truncate text-left text-text-primary`}
|
||||||
|
>
|
||||||
|
{entry.displayLabel}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-3 flex shrink-0 flex-col items-end gap-1.5">
|
||||||
|
{chartData.map((entry) => (
|
||||||
|
<span
|
||||||
|
key={entry.code}
|
||||||
|
className={`${rowClass} tabular-nums text-right text-text-muted`}
|
||||||
|
>
|
||||||
|
{entry.count}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function TodayDonutChart({
|
export function TodayDonutChart({
|
||||||
data,
|
data,
|
||||||
labelForCode,
|
labelForCode,
|
||||||
@@ -26,28 +107,19 @@ export function TodayDonutChart({
|
|||||||
variant = 'donut',
|
variant = 'donut',
|
||||||
sideLegend = false,
|
sideLegend = false,
|
||||||
}: TodayDonutChartProps) {
|
}: TodayDonutChartProps) {
|
||||||
const chartData = data.map((item) => ({
|
const { chartData, resolveColor } = useDonutChartModel({
|
||||||
...item,
|
data,
|
||||||
displayLabel: labelForCode(item.code),
|
labelForCode,
|
||||||
}));
|
colorForCode,
|
||||||
|
swatchStyleForCode,
|
||||||
|
});
|
||||||
|
|
||||||
const resolveColor = (code: string, index: number) =>
|
const innerRadius = variant === 'pie' ? 0 : '62%';
|
||||||
colorForCode?.(code, index) ??
|
const outerRadius = variant === 'pie' ? '88%' : 92;
|
||||||
TODAY_CHART_COLORS[index % TODAY_CHART_COLORS.length];
|
|
||||||
|
|
||||||
const resolveSwatchStyle = (code: string, index: number): CSSProperties =>
|
const pieChart = (
|
||||||
swatchStyleForCode?.(code, index) ?? {
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
backgroundColor: resolveColor(code, index),
|
<PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
|
||||||
borderColor: 'rgba(0, 0, 0, 0.18)',
|
|
||||||
};
|
|
||||||
|
|
||||||
const innerRadius = variant === 'pie' ? 0 : 62;
|
|
||||||
const outerRadius = sideLegend ? 100 : 92;
|
|
||||||
|
|
||||||
const chart = (
|
|
||||||
<TodayChartFrame>
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
|
|
||||||
<Pie
|
<Pie
|
||||||
data={chartData}
|
data={chartData}
|
||||||
dataKey="count"
|
dataKey="count"
|
||||||
@@ -71,83 +143,26 @@ export function TodayDonutChart({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</TodayChartFrame>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!sideLegend) {
|
if (sideLegend) {
|
||||||
return chart;
|
return (
|
||||||
|
<div className="flex h-full min-h-0 w-full items-center gap-3 overflow-hidden sm:gap-4">
|
||||||
|
<div className="flex min-h-0 min-w-0 flex-1 items-center overflow-hidden">
|
||||||
|
<TodayDonutChartLegend
|
||||||
|
data={data}
|
||||||
|
labelForCode={labelForCode}
|
||||||
|
colorForCode={colorForCode}
|
||||||
|
swatchStyleForCode={swatchStyleForCode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="aspect-square h-[min(100%,9.5rem)] w-[min(100%,9.5rem)] shrink-0">
|
||||||
|
{pieChart}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowClass = 'flex h-4 items-center text-xs leading-none';
|
return <TodayChartFrame>{pieChart}</TodayChartFrame>;
|
||||||
const legendInset = 'px-12';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex h-full min-h-0 items-center overflow-hidden ${legendInset}`}>
|
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 items-center overflow-hidden py-0.5">
|
|
||||||
<div className="flex max-h-full min-w-0 flex-col items-start gap-1.5 overflow-y-auto">
|
|
||||||
{chartData.map((entry, index) => (
|
|
||||||
<span key={entry.code} className={rowClass}>
|
|
||||||
<span
|
|
||||||
className="inline-block h-3 w-3 rounded-sm border"
|
|
||||||
style={resolveSwatchStyle(entry.code, index)}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-2 flex min-w-0 flex-col items-start gap-1.5 overflow-hidden">
|
|
||||||
{chartData.map((entry) => (
|
|
||||||
<span
|
|
||||||
key={entry.code}
|
|
||||||
className={`${rowClass} max-w-full truncate text-left text-text-primary`}
|
|
||||||
>
|
|
||||||
{entry.displayLabel}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-3 flex shrink-0 flex-col items-end gap-1.5">
|
|
||||||
{chartData.map((entry) => (
|
|
||||||
<span
|
|
||||||
key={entry.code}
|
|
||||||
className={`${rowClass} tabular-nums text-right text-text-muted`}
|
|
||||||
>
|
|
||||||
{entry.count}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-4 flex h-full min-h-0 w-[min(100%,220px)] max-w-[48%] shrink-0 items-center justify-center">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
|
|
||||||
<Pie
|
|
||||||
data={chartData}
|
|
||||||
dataKey="count"
|
|
||||||
nameKey="displayLabel"
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
innerRadius={innerRadius}
|
|
||||||
outerRadius={outerRadius}
|
|
||||||
paddingAngle={variant === 'pie' ? 1 : 2}
|
|
||||||
stroke="transparent"
|
|
||||||
>
|
|
||||||
{chartData.map((entry, index) => (
|
|
||||||
<Cell key={entry.code} fill={resolveColor(entry.code, index)} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={TODAY_CHART_TOOLTIP_STYLE}
|
|
||||||
formatter={(value, _name, item) => {
|
|
||||||
const row = item?.payload as TodayChartBucket | undefined;
|
|
||||||
return [value, row ? labelForCode(row.code) : ''];
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
import { TodayChartFrame } from '@/components/today/TodayChartFrame';
|
import { TodayChartFrame } from '@/components/today/TodayChartFrame';
|
||||||
import type { TodayChartBucket } from '@/types/today';
|
import type { TodayChartBucket } from '@/types/today';
|
||||||
import {
|
import {
|
||||||
|
chartRankColor,
|
||||||
TODAY_CHART_AXIS_COLOR,
|
TODAY_CHART_AXIS_COLOR,
|
||||||
TODAY_CHART_COLORS,
|
|
||||||
TODAY_CHART_GRID_COLOR,
|
TODAY_CHART_GRID_COLOR,
|
||||||
TODAY_CHART_TOOLTIP_STYLE,
|
TODAY_CHART_TOOLTIP_STYLE,
|
||||||
} from '@/components/today/chart-theme';
|
} from '@/components/today/chart-theme';
|
||||||
@@ -65,7 +65,7 @@ export function TodayHorizontalBarChart({ data }: TodayHorizontalBarChartProps)
|
|||||||
{chartData.map((entry, index) => (
|
{chartData.map((entry, index) => (
|
||||||
<Cell
|
<Cell
|
||||||
key={entry.code}
|
key={entry.code}
|
||||||
fill={TODAY_CHART_COLORS[index % TODAY_CHART_COLORS.length]}
|
fill={chartRankColor(index)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Bar>
|
</Bar>
|
||||||
|
|||||||
130
frontend/src/components/today/TodayLabTaskActivityChart.tsx
Normal file
130
frontend/src/components/today/TodayLabTaskActivityChart.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
AreaChart,
|
||||||
|
CartesianGrid,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
import { TodayChartFrame } from '@/components/today/TodayChartFrame';
|
||||||
|
import {
|
||||||
|
TODAY_CHART_AXIS_COLOR,
|
||||||
|
TODAY_CHART_COMPLETED_COLOR,
|
||||||
|
TODAY_CHART_GRID_COLOR,
|
||||||
|
TODAY_CHART_RECEIVED_COLOR,
|
||||||
|
TODAY_CHART_TOOLTIP_STYLE,
|
||||||
|
} from '@/components/today/chart-theme';
|
||||||
|
import type { TodayStackedDayBucket } from '@/types/today';
|
||||||
|
|
||||||
|
export type LabTaskActivityChartRow = {
|
||||||
|
label: string;
|
||||||
|
completed: number;
|
||||||
|
received: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TodayLabTaskActivityChartProps {
|
||||||
|
data: LabTaskActivityChartRow[];
|
||||||
|
completedLabel: string;
|
||||||
|
receivedLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TodayLabTaskActivityChart({
|
||||||
|
data,
|
||||||
|
completedLabel,
|
||||||
|
receivedLabel,
|
||||||
|
}: TodayLabTaskActivityChartProps) {
|
||||||
|
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 }}>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="labTaskCompletedFill" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor={TODAY_CHART_COMPLETED_COLOR} stopOpacity={0.4} />
|
||||||
|
<stop offset="100%" stopColor={TODAY_CHART_COMPLETED_COLOR} stopOpacity={0.05} />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="labTaskReceivedFill" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor={TODAY_CHART_RECEIVED_COLOR} stopOpacity={0.4} />
|
||||||
|
<stop offset="100%" stopColor={TODAY_CHART_RECEIVED_COLOR} stopOpacity={0.05} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid stroke={TODAY_CHART_GRID_COLOR} vertical={false} />
|
||||||
|
<XAxis
|
||||||
|
dataKey="label"
|
||||||
|
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
|
||||||
|
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
|
||||||
|
tickLine={false}
|
||||||
|
interval={1}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
allowDecimals={false}
|
||||||
|
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
width={32}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
cursor={{ stroke: 'rgba(0, 188, 255, 0.25)' }}
|
||||||
|
contentStyle={TODAY_CHART_TOOLTIP_STYLE}
|
||||||
|
labelFormatter={(label) => String(label)}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="completed"
|
||||||
|
name={completedLabel}
|
||||||
|
stroke={TODAY_CHART_COMPLETED_COLOR}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="url(#labTaskCompletedFill)"
|
||||||
|
dot={{ r: 3, fill: TODAY_CHART_COMPLETED_COLOR, strokeWidth: 0 }}
|
||||||
|
activeDot={{ r: 5, fill: TODAY_CHART_COMPLETED_COLOR }}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="received"
|
||||||
|
name={receivedLabel}
|
||||||
|
stroke={TODAY_CHART_RECEIVED_COLOR}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill="url(#labTaskReceivedFill)"
|
||||||
|
dot={{ r: 3, fill: TODAY_CHART_RECEIVED_COLOR, strokeWidth: 0 }}
|
||||||
|
activeDot={{ r: 5, fill: TODAY_CHART_RECEIVED_COLOR }}
|
||||||
|
/>
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-0.5 flex shrink-0 flex-wrap items-center justify-center gap-x-4 gap-y-0.5 pb-0 text-[11px] text-text-muted">
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="inline-block h-2.5 w-2.5 shrink-0 rounded-sm"
|
||||||
|
style={{ backgroundColor: TODAY_CHART_COMPLETED_COLOR }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
{completedLabel}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="inline-block h-2.5 w-2.5 shrink-0 rounded-sm"
|
||||||
|
style={{ backgroundColor: TODAY_CHART_RECEIVED_COLOR }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
{receivedLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TodayChartFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapLabTaskActivityChartData(
|
||||||
|
buckets: TodayStackedDayBucket[],
|
||||||
|
): LabTaskActivityChartRow[] {
|
||||||
|
return buckets.map((bucket) => ({
|
||||||
|
label: bucket.label,
|
||||||
|
completed: bucket.completed,
|
||||||
|
received: bucket.received,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -18,6 +18,12 @@ interface TodayRadialGaugeChartProps {
|
|||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
fillColor?: string;
|
fillColor?: string;
|
||||||
showRatio?: boolean;
|
showRatio?: boolean;
|
||||||
|
/** Override ring hole size (e.g. "72%" leaves more room for center labels). */
|
||||||
|
innerRadius?: string | number;
|
||||||
|
/** Override compact chart wrapper height class when size is "sm". */
|
||||||
|
compactClassName?: string;
|
||||||
|
/** Ring thickness when size is "sm". */
|
||||||
|
compactBarSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TodayRadialGaugeChart({
|
export function TodayRadialGaugeChart({
|
||||||
@@ -29,20 +35,28 @@ export function TodayRadialGaugeChart({
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
fillColor = TODAY_CHART_PRIMARY_COLOR,
|
fillColor = TODAY_CHART_PRIMARY_COLOR,
|
||||||
showRatio = true,
|
showRatio = true,
|
||||||
|
innerRadius,
|
||||||
|
compactClassName,
|
||||||
|
compactBarSize,
|
||||||
}: TodayRadialGaugeChartProps) {
|
}: TodayRadialGaugeChartProps) {
|
||||||
const isCompact = size === 'sm';
|
const isCompact = size === 'sm';
|
||||||
const clamped = Math.max(0, Math.min(100, percent));
|
const clamped = Math.max(0, Math.min(100, percent));
|
||||||
const data = [{ name: 'progress', value: clamped, fill: fillColor }];
|
const data = [{ name: 'progress', value: clamped, fill: fillColor }];
|
||||||
|
const resolvedInnerRadius = innerRadius ?? (isCompact ? '62%' : '68%');
|
||||||
|
const resolvedBarSize = isCompact ? (compactBarSize ?? 9) : 14;
|
||||||
|
const wrapperClass = isCompact
|
||||||
|
? compactClassName ?? 'h-[108px]'
|
||||||
|
: 'h-full min-h-0 flex-1';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full ${isCompact ? 'h-[108px]' : 'h-full min-h-0 flex-1'}`}>
|
<div className={`relative w-full ${wrapperClass}`}>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RadialBarChart
|
<RadialBarChart
|
||||||
cx="50%"
|
cx="50%"
|
||||||
cy="50%"
|
cy="50%"
|
||||||
innerRadius={isCompact ? '62%' : '68%'}
|
innerRadius={resolvedInnerRadius}
|
||||||
outerRadius="100%"
|
outerRadius="100%"
|
||||||
barSize={isCompact ? 9 : 14}
|
barSize={resolvedBarSize}
|
||||||
data={data}
|
data={data}
|
||||||
startAngle={90}
|
startAngle={90}
|
||||||
endAngle={-270}
|
endAngle={-270}
|
||||||
@@ -55,7 +69,11 @@ export function TodayRadialGaugeChart({
|
|||||||
/>
|
/>
|
||||||
</RadialBarChart>
|
</RadialBarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center px-1">
|
<div
|
||||||
|
className={`pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center ${
|
||||||
|
innerRadius != null && isCompact ? 'px-2.5' : 'px-1'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
className={`font-semibold text-text-primary ${isCompact ? 'text-base leading-tight' : 'text-3xl'}`}
|
className={`font-semibold text-text-primary ${isCompact ? 'text-base leading-tight' : 'text-3xl'}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Bar,
|
|
||||||
BarChart,
|
|
||||||
CartesianGrid,
|
|
||||||
Legend,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Tooltip,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
} from 'recharts';
|
|
||||||
import { TodayChartFrame } from '@/components/today/TodayChartFrame';
|
|
||||||
import type { TodayStackedDayBucket } from '@/types/today';
|
|
||||||
import {
|
|
||||||
TODAY_CHART_AXIS_COLOR,
|
|
||||||
TODAY_CHART_COMPLETED_COLOR,
|
|
||||||
TODAY_CHART_GRID_COLOR,
|
|
||||||
TODAY_CHART_RECEIVED_COLOR,
|
|
||||||
TODAY_CHART_TOOLTIP_STYLE,
|
|
||||||
} from '@/components/today/chart-theme';
|
|
||||||
|
|
||||||
interface TodayStackedBarChartProps {
|
|
||||||
data: TodayStackedDayBucket[];
|
|
||||||
completedLabel: string;
|
|
||||||
receivedLabel: string;
|
|
||||||
formatDayLabel: (code: string) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TodayStackedBarChart({
|
|
||||||
data,
|
|
||||||
completedLabel,
|
|
||||||
receivedLabel,
|
|
||||||
formatDayLabel,
|
|
||||||
}: TodayStackedBarChartProps) {
|
|
||||||
const chartData = data.map((item) => ({
|
|
||||||
...item,
|
|
||||||
dayLabel: formatDayLabel(item.code),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TodayChartFrame>
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart data={chartData} margin={{ top: 8, right: 8, left: -12, bottom: 28 }}>
|
|
||||||
<CartesianGrid stroke={TODAY_CHART_GRID_COLOR} vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="dayLabel"
|
|
||||||
tick={{ fill: TODAY_CHART_AXIS_COLOR, fontSize: 11 }}
|
|
||||||
axisLine={{ stroke: TODAY_CHART_GRID_COLOR }}
|
|
||||||
tickLine={false}
|
|
||||||
/>
|
|
||||||
<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={TODAY_CHART_TOOLTIP_STYLE}
|
|
||||||
labelFormatter={(_, payload) => {
|
|
||||||
const row = payload?.[0]?.payload as TodayStackedDayBucket | undefined;
|
|
||||||
return row ? formatDayLabel(row.code) : '';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Legend
|
|
||||||
verticalAlign="bottom"
|
|
||||||
wrapperStyle={{ fontSize: '12px', color: TODAY_CHART_AXIS_COLOR, paddingTop: 8 }}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="completed"
|
|
||||||
name={completedLabel}
|
|
||||||
stackId="activity"
|
|
||||||
fill={TODAY_CHART_COMPLETED_COLOR}
|
|
||||||
radius={[0, 0, 0, 0]}
|
|
||||||
maxBarSize={48}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="received"
|
|
||||||
name={receivedLabel}
|
|
||||||
stackId="activity"
|
|
||||||
fill={TODAY_CHART_RECEIVED_COLOR}
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
maxBarSize={48}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</TodayChartFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import { formatTimeForInput } from '@/components/appointments/appointmentTime';
|
|||||||
import { purposeLabel } from '@/components/ui/appointments/appointmentPurposeStyles';
|
import { purposeLabel } from '@/components/ui/appointments/appointmentPurposeStyles';
|
||||||
import { treatmentAppointmentHref } from '@/components/shared/treatmentSelection';
|
import { treatmentAppointmentHref } from '@/components/shared/treatmentSelection';
|
||||||
import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay';
|
import { treatmentTypeColor } from '@/components/ui/treatment/treatmentTypeDisplay';
|
||||||
import { canViewTreatment } from '@/components/shared/permissions';
|
import { canViewMyAppointmentsWeekChart } from '@/components/shared/permissions';
|
||||||
import { useAuth } from '@/lib/hooks/useAuth';
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||||||
import { treatmentCatalogApi } from '@/lib/api/treatment-catalog';
|
import { treatmentCatalogApi } from '@/lib/api/treatment-catalog';
|
||||||
import { ListRowSkeleton } from '@/components/today/TodaySkeleton';
|
import { ListRowSkeleton } from '@/components/today/TodaySkeleton';
|
||||||
@@ -41,7 +41,7 @@ export function TodayUpcomingAppointments({
|
|||||||
if (
|
if (
|
||||||
!currentOrganization ||
|
!currentOrganization ||
|
||||||
currentOrganization.type !== 'CLINIC' ||
|
currentOrganization.type !== 'CLINIC' ||
|
||||||
!canViewTreatment(currentOrganization)
|
!canViewMyAppointmentsWeekChart(currentOrganization)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,44 @@ import { CATALOG_PALETTE_COLORS } from '@/components/ui/treatment/catalog-type-c
|
|||||||
/** Chart series colors — same palette as treatment / prosthesis catalog types. */
|
/** Chart series colors — same palette as treatment / prosthesis catalog types. */
|
||||||
export const TODAY_CHART_COLORS = CATALOG_PALETTE_COLORS;
|
export const TODAY_CHART_COLORS = CATALOG_PALETTE_COLORS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rank-based charts (efficiency report, appointments by provider): same hex pool as
|
||||||
|
* CATALOG_PALETTE_COLORS, reordered so consecutive ranks are visually distinct.
|
||||||
|
*/
|
||||||
|
const CHART_RANK_COLOR_ORDER = [
|
||||||
|
'#fed7aa', // peach
|
||||||
|
'#93c5fd', // blue
|
||||||
|
'#86efac', // green
|
||||||
|
'#c4b5fd', // purple
|
||||||
|
'#f9a8d4', // pink
|
||||||
|
'#bae6fd', // sky
|
||||||
|
'#fde68a', // yellow
|
||||||
|
'#99f6e4', // teal
|
||||||
|
'#fca5a5', // salmon
|
||||||
|
'#ddd6fe', // lavender
|
||||||
|
'#fdba74', // orange — separated from peach
|
||||||
|
'#a5b4fc', // indigo
|
||||||
|
'#cbd5e1', // slate
|
||||||
|
'#d9f99d', // lime
|
||||||
|
'#fecaca', // light coral
|
||||||
|
'#fbcfe8', // pale pink
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const chartRankColorSet = new Set<string>(CHART_RANK_COLOR_ORDER);
|
||||||
|
|
||||||
|
export const TODAY_CHART_RANK_COLORS: readonly string[] = [
|
||||||
|
...CHART_RANK_COLOR_ORDER,
|
||||||
|
...CATALOG_PALETTE_COLORS.filter((color) => !chartRankColorSet.has(color)),
|
||||||
|
];
|
||||||
|
|
||||||
|
export function chartRankColor(index: number): string {
|
||||||
|
return TODAY_CHART_RANK_COLORS[index % TODAY_CHART_RANK_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
/** Primary accent for single-series charts (area, gauge). */
|
/** Primary accent for single-series charts (area, gauge). */
|
||||||
export const TODAY_CHART_PRIMARY_COLOR = CATALOG_PALETTE_COLORS[5] ?? '#c4b5fd';
|
export const TODAY_CHART_PRIMARY_COLOR = CATALOG_PALETTE_COLORS[5] ?? '#c4b5fd';
|
||||||
|
|
||||||
/** Stacked bar segments for lab task activity. */
|
/** Lab task activity series (completed / received). */
|
||||||
export const TODAY_CHART_COMPLETED_COLOR = CATALOG_PALETTE_COLORS[8] ?? '#86efac';
|
export const TODAY_CHART_COMPLETED_COLOR = CATALOG_PALETTE_COLORS[8] ?? '#86efac';
|
||||||
export const TODAY_CHART_RECEIVED_COLOR = CATALOG_PALETTE_COLORS[11] ?? '#bae6fd';
|
export const TODAY_CHART_RECEIVED_COLOR = CATALOG_PALETTE_COLORS[11] ?? '#bae6fd';
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ export const TODAY_DASHBOARD_LAYOUT = {
|
|||||||
kpi: { width: 1, height: 1 },
|
kpi: { width: 1, height: 1 },
|
||||||
subscription: { width: 1, height: 2 },
|
subscription: { width: 1, height: 2 },
|
||||||
upcoming: { width: 2, height: 3 },
|
upcoming: { width: 2, height: 3 },
|
||||||
/** Tall charts: area, stacked bar, pie with side legend */
|
/** Week area charts (appointments, lab task activity). */
|
||||||
|
chartArea: { width: 2, height: 2 },
|
||||||
|
/** Vertical / horizontal bar charts. */
|
||||||
|
chartBar: { width: 2, height: 3 },
|
||||||
|
/** @deprecated Prefer chartArea (height 2) or chartBar (height 3). */
|
||||||
chart: { width: 2, height: 3 },
|
chart: { width: 2, height: 3 },
|
||||||
/** Medium charts: horizontal bar, vertical bar, radial gauge */
|
/** @deprecated Use chartArea or chartBar */
|
||||||
chartMedium: { width: 2, height: 2 },
|
chartMedium: { width: 2, height: 2 },
|
||||||
} as const satisfies Record<string, TodayDashboardLayout>;
|
} as const satisfies Record<string, TodayDashboardLayout>;
|
||||||
|
|
||||||
|
|||||||
@@ -99,19 +99,6 @@ export const TODAY_KPI_DEFINITIONS: TodayKpiDefinition[] = [
|
|||||||
return count === null ? null : String(count);
|
return count === null ? null : String(count);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'draftTreatments',
|
|
||||||
titleKey: 'widgetDraftTreatments',
|
|
||||||
icon: ClipboardList,
|
|
||||||
color: 'yellow',
|
|
||||||
orgTypes: ['CLINIC'],
|
|
||||||
href: '/treatment',
|
|
||||||
isVisible: (org) => canViewTreatment(org),
|
|
||||||
formatValue: (widgets) => {
|
|
||||||
const count = countWidget(widgets, 'draftTreatments');
|
|
||||||
return count === null ? null : String(count);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'labCasesPendingSend',
|
key: 'labCasesPendingSend',
|
||||||
titleKey: 'widgetLabCasesPendingSend',
|
titleKey: 'widgetLabCasesPendingSend',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Shared pastel palette for treatment types, prosthesis types, and dashboard charts.
|
* Shared pastel palette for treatment types, prosthesis types, and dashboard charts.
|
||||||
* Treatment types own the canonical hex values; prosthesis types reuse the same codes.
|
* Treatment and prosthesis each have dedicated hex maps — prosthesis colors are unique
|
||||||
|
* within the prosthesis catalog (no duplicate swatches on charts or badges).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const TREATMENT_TYPE_COLORS: Record<string, string> = {
|
export const TREATMENT_TYPE_COLORS: Record<string, string> = {
|
||||||
@@ -19,28 +20,28 @@ export const TREATMENT_TYPE_COLORS: Record<string, string> = {
|
|||||||
continue_treatment: '#99f6e4',
|
continue_treatment: '#99f6e4',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Prosthesis codes mapped to treatment-palette hex values (mapping is arbitrary). */
|
/** Dedicated prosthesis palette — one distinct pastel per catalog code. */
|
||||||
export const PROSTHESIS_TYPE_COLORS: Record<string, string> = {
|
export const PROSTHESIS_TYPE_COLORS: Record<string, string> = {
|
||||||
pfm_crown: '#cbd5e1',
|
pfm_crown: '#e2e8f0',
|
||||||
pfz_crown: '#86efac',
|
pfz_crown: '#bbf7d0',
|
||||||
monolithic_zirconia: '#99f6e4',
|
monolithic_zirconia: '#e0f2fe',
|
||||||
glass_ceramic_crown: '#fde68a',
|
glass_ceramic_crown: '#fef08a',
|
||||||
full_metal_crown: '#cbd5e1',
|
full_metal_crown: '#d4d4d8',
|
||||||
temporary_resin_crown: '#bae6fd',
|
temporary_resin_crown: '#bae6fd',
|
||||||
pmma: '#93c5fd',
|
pmma: '#7dd3fc',
|
||||||
peek_crown: '#99f6e4',
|
peek_crown: '#5eead4',
|
||||||
veneer_zirconia: '#86efac',
|
veneer_zirconia: '#6ee7b7',
|
||||||
veneer_ips_press: '#fed7aa',
|
veneer_ips_press: '#fed7aa',
|
||||||
veneer_ips_cad: '#fdba74',
|
veneer_ips_cad: '#fdba74',
|
||||||
soft_structure: '#ddd6fe',
|
soft_structure: '#ddd6fe',
|
||||||
customized_abutment: '#a5b4fc',
|
customized_abutment: '#a5b4fc',
|
||||||
prefabricated_abutment: '#93c5fd',
|
prefabricated_abutment: '#c7d2fe',
|
||||||
ti_base_abutment: '#bae6fd',
|
ti_base_abutment: '#bfdbfe',
|
||||||
multi_unit_abutment: '#a5b4fc',
|
multi_unit_abutment: '#818cf8',
|
||||||
zirconia_abutment: '#86efac',
|
zirconia_abutment: '#34d399',
|
||||||
screw_retained: '#c4b5fd',
|
screw_retained: '#e9d5ff',
|
||||||
zirconia_overlay: '#99f6e4',
|
zirconia_overlay: '#2dd4bf',
|
||||||
ips_overlay: '#fde68a',
|
ips_overlay: '#fef3c7',
|
||||||
smile_design: '#f9a8d4',
|
smile_design: '#f9a8d4',
|
||||||
mockup: '#fbcfe8',
|
mockup: '#fbcfe8',
|
||||||
};
|
};
|
||||||
@@ -54,7 +55,12 @@ export const CATALOG_FALLBACK_COLORS = [
|
|||||||
'#fbcfe8',
|
'#fbcfe8',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** Ordered palette for charts and rotating unknown catalog codes. */
|
/** Fallback rotation for unknown prosthesis codes — drawn from the prosthesis palette. */
|
||||||
|
export const PROSTHESIS_FALLBACK_COLORS: readonly string[] = [
|
||||||
|
...new Set(Object.values(PROSTHESIS_TYPE_COLORS)),
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Ordered palette for charts and rotating unknown treatment catalog codes. */
|
||||||
export const CATALOG_PALETTE_COLORS: readonly string[] = [
|
export const CATALOG_PALETTE_COLORS: readonly string[] = [
|
||||||
'#fed7aa',
|
'#fed7aa',
|
||||||
'#fdba74',
|
'#fdba74',
|
||||||
@@ -72,12 +78,25 @@ export const CATALOG_PALETTE_COLORS: readonly string[] = [
|
|||||||
'#ddd6fe',
|
'#ddd6fe',
|
||||||
'#d9f99d',
|
'#d9f99d',
|
||||||
'#fbcfe8',
|
'#fbcfe8',
|
||||||
|
...PROSTHESIS_FALLBACK_COLORS.filter(
|
||||||
|
(color) =>
|
||||||
|
![
|
||||||
|
'#fed7aa',
|
||||||
|
'#fdba74',
|
||||||
|
'#bae6fd',
|
||||||
|
'#f9a8d4',
|
||||||
|
'#ddd6fe',
|
||||||
|
'#fbcfe8',
|
||||||
|
'#a5b4fc',
|
||||||
|
].includes(color),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
export function resolveCatalogTypeColor(
|
export function resolveCatalogTypeColor(
|
||||||
code: string,
|
code: string,
|
||||||
colorMap: Record<string, string>,
|
colorMap: Record<string, string>,
|
||||||
index = 0,
|
index = 0,
|
||||||
|
fallbackColors: readonly string[] = CATALOG_FALLBACK_COLORS,
|
||||||
): string {
|
): string {
|
||||||
return colorMap[code] ?? CATALOG_FALLBACK_COLORS[index % CATALOG_FALLBACK_COLORS.length];
|
return colorMap[code] ?? fallbackColors[index % fallbackColors.length];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import {
|
import {
|
||||||
|
PROSTHESIS_FALLBACK_COLORS,
|
||||||
PROSTHESIS_TYPE_COLORS,
|
PROSTHESIS_TYPE_COLORS,
|
||||||
resolveCatalogTypeColor,
|
resolveCatalogTypeColor,
|
||||||
} from '@/components/ui/treatment/catalog-type-colors';
|
} from '@/components/ui/treatment/catalog-type-colors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prosthesis-type colors for lab-facing surfaces (Tasks list, Cases detail group
|
* Prosthesis-type colors for lab-facing surfaces (Tasks list, Cases detail group
|
||||||
* headers / badges). Uses the same hex palette as treatment types.
|
* headers / badges). Uses a dedicated pastel map (unique per prosthesis code).
|
||||||
*
|
*
|
||||||
* Clinic-facing dispatch flows intentionally do NOT use these colors.
|
* Clinic-facing dispatch flows intentionally do NOT use these colors.
|
||||||
*/
|
*/
|
||||||
@@ -15,7 +16,7 @@ import {
|
|||||||
const BADGE_INK = '#14253d';
|
const BADGE_INK = '#14253d';
|
||||||
|
|
||||||
export function prosthesisTypeColor(code: string, index = 0): string {
|
export function prosthesisTypeColor(code: string, index = 0): string {
|
||||||
return resolveCatalogTypeColor(code, PROSTHESIS_TYPE_COLORS, index);
|
return resolveCatalogTypeColor(code, PROSTHESIS_TYPE_COLORS, index, PROSTHESIS_FALLBACK_COLORS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Filled swatch (small indicator dots). */
|
/** Filled swatch (small indicator dots). */
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export type TodayStackedDayBucket = {
|
|||||||
|
|
||||||
export type TodaySummaryCharts = {
|
export type TodaySummaryCharts = {
|
||||||
treatmentMixWeek?: TodayChartBucket[];
|
treatmentMixWeek?: TodayChartBucket[];
|
||||||
tasksByWorkflowStep?: TodayChartBucket[];
|
tasksByProsthesis?: TodayChartBucket[];
|
||||||
appointmentsByProvider?: TodayChartBucket[];
|
appointmentsByProvider?: TodayChartBucket[];
|
||||||
caseCompletion?: {
|
caseCompletion?: {
|
||||||
completed: number;
|
completed: number;
|
||||||
@@ -35,7 +35,6 @@ export type TodaySummaryCharts = {
|
|||||||
appointmentsWeekAll?: TodayChartBucket[];
|
appointmentsWeekAll?: TodayChartBucket[];
|
||||||
appointmentsWeekMine?: TodayChartBucket[];
|
appointmentsWeekMine?: TodayChartBucket[];
|
||||||
labTaskActivityWeek?: TodayStackedDayBucket[];
|
labTaskActivityWeek?: TodayStackedDayBucket[];
|
||||||
inProgressTasksByProsthesis?: TodayChartBucket[];
|
|
||||||
efficiencyReport?: TodayChartBucket[];
|
efficiencyReport?: TodayChartBucket[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ export type TodayWidgetKey =
|
|||||||
| 'appointmentsToday'
|
| 'appointmentsToday'
|
||||||
| 'patientsToday'
|
| 'patientsToday'
|
||||||
| 'treatmentsToday'
|
| 'treatmentsToday'
|
||||||
| 'draftTreatments'
|
|
||||||
| 'labCasesPendingSend'
|
| 'labCasesPendingSend'
|
||||||
| 'casesReceivedToday'
|
| 'casesReceivedToday'
|
||||||
| 'casesInProgress'
|
| 'casesInProgress'
|
||||||
|
|||||||
Reference in New Issue
Block a user