2026-07-11 00:07:48 +03:30
|
|
|
import type { LucideIcon } from 'lucide-react';
|
|
|
|
|
import {
|
|
|
|
|
AlertCircle,
|
|
|
|
|
CalendarDays,
|
|
|
|
|
ClipboardList,
|
|
|
|
|
FlaskConical,
|
|
|
|
|
Link2,
|
|
|
|
|
Stethoscope,
|
|
|
|
|
UserCog,
|
|
|
|
|
Users,
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
import type { Organization } from '@/types/organization';
|
|
|
|
|
import {
|
|
|
|
|
canEditStaff,
|
2026-07-11 22:32:37 +03:30
|
|
|
canViewAppointmentsTab,
|
2026-07-11 00:07:48 +03:30
|
|
|
canViewCases,
|
|
|
|
|
canViewStaff,
|
|
|
|
|
canViewTasks,
|
|
|
|
|
canViewTreatment,
|
|
|
|
|
hasPermission,
|
|
|
|
|
type OrgTypeName,
|
|
|
|
|
} from '@/components/shared/permissions';
|
|
|
|
|
import type { TodaySummaryWidgets, TodayWidgetKey } from '@/types/today';
|
|
|
|
|
|
|
|
|
|
export type KpiCardColor = 'blue' | 'yellow' | 'green' | 'red' | 'purple' | 'default';
|
|
|
|
|
|
|
|
|
|
export interface TodayKpiDefinition {
|
|
|
|
|
key: TodayWidgetKey;
|
|
|
|
|
titleKey: string;
|
|
|
|
|
icon: LucideIcon;
|
|
|
|
|
color: KpiCardColor;
|
|
|
|
|
orgTypes: OrgTypeName[];
|
2026-07-11 00:48:35 +03:30
|
|
|
href: string;
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org: Organization | null) => boolean;
|
|
|
|
|
formatValue: (widgets: TodaySummaryWidgets) => string | null;
|
|
|
|
|
formatSubtitle?: (widgets: TodaySummaryWidgets) => string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countWidget(
|
|
|
|
|
widgets: TodaySummaryWidgets,
|
|
|
|
|
key: TodayWidgetKey,
|
|
|
|
|
): number | null {
|
|
|
|
|
const value = widgets[key];
|
|
|
|
|
if (!value || !('count' in value)) return null;
|
|
|
|
|
return value.count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canManageOrganizations(org: Organization | null): boolean {
|
|
|
|
|
if (!org) return false;
|
|
|
|
|
if (org.isOwner) return true;
|
|
|
|
|
return hasPermission(org, 'TAB_ORGANIZATIONS_EDIT');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canViewPatients(org: Organization | null): boolean {
|
|
|
|
|
if (!org) return false;
|
|
|
|
|
return (
|
|
|
|
|
hasPermission(org, 'TAB_PATIENTS_READ') ||
|
|
|
|
|
hasPermission(org, 'TAB_PATIENTS_EDIT')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const TODAY_KPI_DEFINITIONS: TodayKpiDefinition[] = [
|
|
|
|
|
{
|
|
|
|
|
key: 'appointmentsToday',
|
|
|
|
|
titleKey: 'widgetAppointmentsToday',
|
|
|
|
|
icon: CalendarDays,
|
|
|
|
|
color: 'blue',
|
|
|
|
|
orgTypes: ['CLINIC'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/appointments',
|
2026-07-11 22:32:37 +03:30
|
|
|
isVisible: (org) => canViewAppointmentsTab(org),
|
2026-07-11 00:07:48 +03:30
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'appointmentsToday');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'patientsToday',
|
|
|
|
|
titleKey: 'widgetPatientsToday',
|
|
|
|
|
icon: Users,
|
|
|
|
|
color: 'green',
|
|
|
|
|
orgTypes: ['CLINIC'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/patients',
|
2026-07-11 22:32:37 +03:30
|
|
|
isVisible: (org) => canViewPatients(org) || canViewAppointmentsTab(org),
|
2026-07-11 00:07:48 +03:30
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'patientsToday');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'treatmentsToday',
|
|
|
|
|
titleKey: 'widgetTreatmentsToday',
|
|
|
|
|
icon: Stethoscope,
|
|
|
|
|
color: 'purple',
|
|
|
|
|
orgTypes: ['CLINIC'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/treatment',
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org) => canViewTreatment(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'treatmentsToday');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'labCasesPendingSend',
|
|
|
|
|
titleKey: 'widgetLabCasesPendingSend',
|
|
|
|
|
icon: FlaskConical,
|
|
|
|
|
color: 'red',
|
|
|
|
|
orgTypes: ['CLINIC'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/treatment',
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org) => canViewTreatment(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'labCasesPendingSend');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-11 03:12:56 +03:30
|
|
|
{
|
|
|
|
|
key: 'providersWithoutWorkingHours',
|
|
|
|
|
titleKey: 'widgetProvidersWithoutWorkingHours',
|
|
|
|
|
icon: UserCog,
|
|
|
|
|
color: 'yellow',
|
|
|
|
|
orgTypes: ['CLINIC'],
|
|
|
|
|
href: '/staff',
|
|
|
|
|
isVisible: (org) => canViewStaff(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'providersWithoutWorkingHours');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-11 00:07:48 +03:30
|
|
|
{
|
|
|
|
|
key: 'casesReceivedToday',
|
|
|
|
|
titleKey: 'widgetCasesReceivedToday',
|
|
|
|
|
icon: FlaskConical,
|
|
|
|
|
color: 'blue',
|
|
|
|
|
orgTypes: ['LAB'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/cases',
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org) => canViewCases(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'casesReceivedToday');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-11 03:12:56 +03:30
|
|
|
{
|
|
|
|
|
key: 'casesInProgress',
|
|
|
|
|
titleKey: 'widgetCasesInProgress',
|
|
|
|
|
icon: FlaskConical,
|
|
|
|
|
color: 'yellow',
|
|
|
|
|
orgTypes: ['LAB'],
|
|
|
|
|
href: '/cases',
|
|
|
|
|
isVisible: (org) => canViewCases(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'casesInProgress');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-07-11 00:07:48 +03:30
|
|
|
{
|
|
|
|
|
key: 'tasksInProgress',
|
|
|
|
|
titleKey: 'widgetTasksInProgress',
|
|
|
|
|
icon: ClipboardList,
|
|
|
|
|
color: 'yellow',
|
|
|
|
|
orgTypes: ['LAB'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/tasks',
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org) => canViewTasks(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'tasksInProgress');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'importantTasks',
|
|
|
|
|
titleKey: 'widgetImportantTasks',
|
|
|
|
|
icon: AlertCircle,
|
|
|
|
|
color: 'red',
|
|
|
|
|
orgTypes: ['LAB'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/tasks',
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org) => canViewTasks(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'importantTasks');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'pendingConnections',
|
|
|
|
|
titleKey: 'widgetPendingConnections',
|
|
|
|
|
icon: Link2,
|
|
|
|
|
color: 'yellow',
|
|
|
|
|
orgTypes: ['CLINIC', 'LAB'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/organizations',
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org) => canManageOrganizations(org),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'pendingConnections');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'pendingStaffInvites',
|
|
|
|
|
titleKey: 'widgetPendingStaffInvites',
|
|
|
|
|
icon: UserCog,
|
|
|
|
|
color: 'purple',
|
|
|
|
|
orgTypes: ['CLINIC', 'LAB'],
|
2026-07-11 00:48:35 +03:30
|
|
|
href: '/staff',
|
2026-07-11 00:07:48 +03:30
|
|
|
isVisible: (org) => canEditStaff(org) || Boolean(org?.isOwner),
|
|
|
|
|
formatValue: (widgets) => {
|
|
|
|
|
const count = countWidget(widgets, 'pendingStaffInvites');
|
|
|
|
|
return count === null ? null : String(count);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export function getEligibleTodayKpis(org: Organization | null): TodayKpiDefinition[] {
|
|
|
|
|
if (!org) return [];
|
|
|
|
|
|
|
|
|
|
return TODAY_KPI_DEFINITIONS.filter((definition) => {
|
|
|
|
|
if (!definition.orgTypes.includes(org.type)) return false;
|
|
|
|
|
return definition.isVisible(org);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getVisibleTodayKpis(
|
|
|
|
|
org: Organization | null,
|
|
|
|
|
widgets: TodaySummaryWidgets,
|
|
|
|
|
): TodayKpiDefinition[] {
|
|
|
|
|
return getEligibleTodayKpis(org).filter(
|
|
|
|
|
(definition) => definition.formatValue(widgets) !== null,
|
|
|
|
|
);
|
|
|
|
|
}
|