/** Tab permissions — keep in sync with prisma seed and AuthService ALL_PERMISSIONS */ export const ALL_TAB_PERMISSIONS = [ 'TAB_TODAY_READ', 'TAB_TODAY_EDIT', 'TAB_STAFF_READ', 'TAB_STAFF_EDIT', 'TAB_ORGANIZATIONS_READ', 'TAB_ORGANIZATIONS_EDIT', 'TAB_PATIENTS_READ', 'TAB_PATIENTS_EDIT', 'TAB_APPOINTMENTS_READ', 'TAB_APPOINTMENTS_EDIT', 'TAB_TREATMENT_READ', 'TAB_TREATMENT_EDIT', 'TAB_CASES_READ', 'TAB_CASES_EDIT', 'TAB_BILLING_READ', 'TAB_BILLING_EDIT', 'TAB_REPORTS_READ', 'TAB_REPORTS_EDIT', ] as const; export type TabPermission = (typeof ALL_TAB_PERMISSIONS)[number]; const ALL_TAB_SET = new Set(ALL_TAB_PERMISSIONS); const TAB_ORDER_INDEX = new Map( ALL_TAB_PERMISSIONS.map((p, i) => [p, i]), ); /** Enterprise / unlimited seat plans use this sentinel in seed data */ export const SEAT_UNLIMITED_THRESHOLD = 999999; export function isUnlimitedSeats(maxUsers: number): boolean { return maxUsers >= SEAT_UNLIMITED_THRESHOLD; } /** EDIT implies READ for the same feature tab */ const EDIT_TO_READ: Record = { TAB_TODAY_EDIT: 'TAB_TODAY_READ', TAB_PATIENTS_EDIT: 'TAB_PATIENTS_READ', TAB_APPOINTMENTS_EDIT: 'TAB_APPOINTMENTS_READ', TAB_STAFF_EDIT: 'TAB_STAFF_READ', TAB_ORGANIZATIONS_EDIT: 'TAB_ORGANIZATIONS_READ', TAB_TREATMENT_EDIT: 'TAB_TREATMENT_READ', TAB_CASES_EDIT: 'TAB_CASES_READ', TAB_BILLING_EDIT: 'TAB_BILLING_READ', TAB_REPORTS_EDIT: 'TAB_REPORTS_READ', }; /** * Dedupe, drop unknown strings, and add implied READ permissions for each EDIT. */ export function normalizeTabPermissions(names: string[]): string[] { const out = new Set(); for (const raw of names) { const n = typeof raw === 'string' ? raw.trim() : ''; if (!n || !ALL_TAB_SET.has(n)) continue; out.add(n); const read = EDIT_TO_READ[n]; if (read) out.add(read); } return [...out].sort((a, b) => (TAB_ORDER_INDEX.get(a) ?? 0) - (TAB_ORDER_INDEX.get(b) ?? 0)); }