diff --git a/frontend/src/components/shared/hidden-tabs.ts b/frontend/src/components/shared/hidden-tabs.ts new file mode 100644 index 0000000..6b76673 --- /dev/null +++ b/frontend/src/components/shared/hidden-tabs.ts @@ -0,0 +1,44 @@ +/** + * Tabs hidden from navigation and staff permission UI until features ship. + * Routes, pages, and backend permissions stay in place — flip flags to re-enable. + */ +export const HIDDEN_TAB_FEATURE_FLAGS = { + billing: true, + reports: true, +} as const; + +const HIDDEN_PATHS: readonly string[] = [ + ...(HIDDEN_TAB_FEATURE_FLAGS.billing ? (['/billing'] as const) : []), + ...(HIDDEN_TAB_FEATURE_FLAGS.reports ? (['/reports'] as const) : []), +]; + +const HIDDEN_PERMISSIONS: readonly string[] = [ + ...(HIDDEN_TAB_FEATURE_FLAGS.billing + ? (['TAB_BILLING_READ', 'TAB_BILLING_EDIT'] as const) + : []), + ...(HIDDEN_TAB_FEATURE_FLAGS.reports + ? (['TAB_REPORTS_READ', 'TAB_REPORTS_EDIT'] as const) + : []), +]; + +const HIDDEN_PATH_SET = new Set(HIDDEN_PATHS); +const HIDDEN_PERMISSION_SET = new Set(HIDDEN_PERMISSIONS); + +export function isHiddenTabPath(path: string): boolean { + return HIDDEN_PATH_SET.has(path); +} + +export function isHiddenTabPermission(permission: string): boolean { + return HIDDEN_PERMISSION_SET.has(permission); +} + +/** Keep hidden-tab grants when saving staff permissions the UI no longer shows. */ +export function preserveHiddenTabPermissions( + fromForm: string[], + existing?: string[] | null, +): string[] { + if (!existing?.length) return fromForm; + const preserved = existing.filter((p) => isHiddenTabPermission(p)); + if (!preserved.length) return fromForm; + return [...new Set([...fromForm, ...preserved])]; +} diff --git a/frontend/src/components/shared/permissions.ts b/frontend/src/components/shared/permissions.ts index cad5b75..b7c4631 100644 --- a/frontend/src/components/shared/permissions.ts +++ b/frontend/src/components/shared/permissions.ts @@ -1,4 +1,5 @@ import type { Organization } from '@/types/organization'; +import { isHiddenTabPath } from '@/components/shared/hidden-tabs'; export type OrgTypeName = 'CLINIC' | 'LAB'; @@ -64,6 +65,10 @@ export function canAccessDashboardRoute(org: Organization | null, pathname: stri const route = getRouteConfigForPath(pathname); if (!route) return true; + if (isHiddenTabPath(route.prefix)) { + return false; + } + if (!isRouteAllowedForOrgType(pathname, org.type)) { return false; } @@ -89,6 +94,7 @@ export function firstAccessibleDashboardPath(org: Organization | null): string { for (const route of DASHBOARD_ROUTES) { if (!route.orgTypes.includes(org.type)) continue; + if (isHiddenTabPath(route.prefix)) continue; if (route.prefix === '/appointments') { if (canAccessAppointmentsSection(org)) return route.prefix; continue; diff --git a/frontend/src/components/staff/staff-permission-form.ts b/frontend/src/components/staff/staff-permission-form.ts index 5f4c04a..7e66e65 100644 --- a/frontend/src/components/staff/staff-permission-form.ts +++ b/frontend/src/components/staff/staff-permission-form.ts @@ -4,6 +4,7 @@ */ import type { OrgTypeName } from '@/components/shared/permissions'; +import { isHiddenTabPermission } from '@/components/shared/hidden-tabs'; export const STAFF_FEATURE_GROUPS = [ { labelKey: 'featureToday', read: 'TAB_TODAY_READ', edit: 'TAB_TODAY_EDIT', orgTypes: ['CLINIC', 'LAB'] as const }, @@ -24,8 +25,9 @@ export type OrgType = OrgTypeName | null | undefined; type StaffFeaturesTranslate = (key: string) => string; export function staffFeatureGroupsForOrgType(organizationType: OrgType) { - if (!organizationType) return [...STAFF_FEATURE_GROUPS]; - return STAFF_FEATURE_GROUPS.filter((g) => + const visible = STAFF_FEATURE_GROUPS.filter((g) => !isHiddenTabPermission(g.read)); + if (!organizationType) return [...visible]; + return visible.filter((g) => (g.orgTypes as readonly OrgTypeName[]).includes(organizationType), ); } diff --git a/frontend/src/components/ui/shared/Sidebar.tsx b/frontend/src/components/ui/shared/Sidebar.tsx index 4b2763f..027d3a7 100644 --- a/frontend/src/components/ui/shared/Sidebar.tsx +++ b/frontend/src/components/ui/shared/Sidebar.tsx @@ -27,6 +27,7 @@ import { canViewTasks, canViewTab, } from '@/components/shared/permissions'; +import { isHiddenTabPath } from '@/components/shared/hidden-tabs'; import { counterpartOrganizationType, organizationTypeIcon, @@ -82,6 +83,9 @@ function Sidebar({ mobileOpen = false, onClose }: SidebarProps) { const visibleMenu = useMemo( () => menu.filter((item) => { + if (isHiddenTabPath(item.path)) { + return false; + } if (!orgType || !item.orgTypes.includes(orgType)) { return false; } diff --git a/frontend/src/components/ui/staff/StaffPage.tsx b/frontend/src/components/ui/staff/StaffPage.tsx index 866b049..e007f00 100644 --- a/frontend/src/components/ui/staff/StaffPage.tsx +++ b/frontend/src/components/ui/staff/StaffPage.tsx @@ -9,6 +9,7 @@ import { canEditStaff, canViewStaff, } from '@/components/shared/permissions'; +import { preserveHiddenTabPermissions } from '@/components/shared/hidden-tabs'; import { permissionNamesFromFeatureState, emptyFeaturePermissionState, @@ -464,7 +465,10 @@ export function StaffPage() { await staffApi.updateMember(editing.id, { name: editName.trim(), - permissionNames: permissionNamesFromFeatureState(editPerms), + permissionNames: preserveHiddenTabPermissions( + permissionNamesFromFeatureState(editPerms), + editing.permissions, + ), }); toast.showSuccess(t('successMemberUpdated'));