From 6a54aa6c509abec36686f3136130ffa3ecb60f0e Mon Sep 17 00:00:00 2001 From: shancheas Date: Tue, 1 Sep 2026 13:15:12 +0700 Subject: [PATCH] feat: update privilege keys and module configurations for enhanced access control - Refactored privilege keys in `api.md` to use a more structured naming convention, aligning with the new `Group.Parent.Module` format. - Updated various module configurations across the application to reflect the new privilege key structure, ensuring consistent access control. - Removed deprecated keys and streamlined the privilege management process, enhancing clarity and maintainability. - Added new tests for privilege key parsing and grouping functionalities to ensure reliability and correctness. These changes significantly improve the application's privilege management system, providing a clearer structure for access control and enhancing overall security. --- .cursor/rules/web-rbac.mdc | 10 +- api.md | 53 +++--- apps/web/src/apps/auth/login/index.tsx | 40 ---- .../src/apps/main/layouts/data/menu.data.ts | 38 ++-- .../data/branch.remote.service.test.ts | 2 +- .../domain/constants/branch.constants.ts | 2 +- .../data/customer.remote.service.test.ts | 2 +- .../domain/constants/customer.constants.ts | 2 +- .../data/division.remote.service.test.ts | 2 +- .../domain/constants/division.constants.ts | 2 +- .../data/employee.remote.service.test.ts | 2 +- .../constants/employee.constants.test.ts | 4 +- .../domain/constants/employee.constants.ts | 2 +- .../data/product.remote.service.test.ts | 2 +- .../domain/constants/product.constants.ts | 2 +- .../cycles/data/cycle.remote.service.test.ts | 2 +- .../domain/constants/cycle.constants.ts | 2 +- .../domain/constants/reports.constants.ts | 2 +- .../constants/packing-slip.constants.ts | 2 +- .../plans/data/plan.remote.service.test.ts | 2 +- .../plans/domain/constants/plan.constants.ts | 2 +- .../modules/field/shared/lookup.factories.ts | 4 +- .../data/sales-invoice.remote.service.test.ts | 2 +- .../constants/sales-invoice.constants.ts | 2 +- .../data/sales-order.remote.service.test.ts | 2 +- .../domain/constants/sales-order.constants.ts | 2 +- .../data/sales-payment.remote.service.test.ts | 2 +- .../constants/sales-payment.constants.ts | 2 +- .../domain/constants/reports.constants.ts | 2 +- .../data/sales-request.remote.service.test.ts | 2 +- .../constants/sales-request.constants.ts | 2 +- .../sales-document.remote.service.test.ts | 2 +- .../data/privilege.remote.service.test.ts | 6 +- .../domain/constants/privilege.constants.ts | 2 +- .../domain/privilege-key-hierarchy.test.ts | 82 ++++++++ .../domain/privilege-key-hierarchy.ts | 140 ++++++++++++++ .../domain/privilege.matrix-rows.test.ts | 2 +- .../privilege.remote.transformer.test.ts | 6 +- .../form-component/form-permissions.tsx | 57 +++--- .../components/privilege-matrix-layout.tsx | 179 ++++++++++++++++++ .../privilege-permissions-table.tsx | 52 ++--- .../presentation/languages/en/privileges.json | 11 ++ .../presentation/languages/id/privileges.json | 11 ++ .../users/data/user.remote.service.test.ts | 2 +- .../users/domain/constants/user.constants.ts | 2 +- apps/web/src/core/constants/module-key.ts | 28 ++- apps/web/src/core/lib/auth.helper.test.ts | 4 +- .../lib/enterprise-storage-adapter.test.ts | 6 +- .../lib/filter-menu-by-view-privilege.test.ts | 14 +- .../src/core/lib/map-user-privileges.test.ts | 10 +- .../lib/trackgo-remote-data-services.test.ts | 2 +- 51 files changed, 592 insertions(+), 225 deletions(-) create mode 100644 apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.test.ts create mode 100644 apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.ts create mode 100644 apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx diff --git a/.cursor/rules/web-rbac.mdc b/.cursor/rules/web-rbac.mdc index d2b64de..5b8208d 100644 --- a/.cursor/rules/web-rbac.mdc +++ b/.cursor/rules/web-rbac.mdc @@ -8,11 +8,11 @@ alwaysApply: false Every authenticated product module in `apps/web` must be gated by `GET /auth/me` permissions. Copy Privileges (`system/privileges`) — do not invent a second RBAC path. -Catalog keys live in `api.md` §4 (`PRIVILEGES`, `CONFIGURATION.BRANCH`, `SALES.ORDER`, …). `isSuperadmin` bypasses the matrix (adapter returns `defaultPrivileges`). +Catalog keys use `Group.Parent.Module` or `Group.Parent.Module.Submodule` (e.g. `ADMIN.SETTINGS.DATA.BRANCH`, `ADMIN.SALES.ACTIVITIES.ORDER`). `isSuperadmin` bypasses the matrix (adapter returns `defaultPrivileges`). ## Required wiring (do all four) -1. **`moduleKey`** on `ModuleConfigEntity` equals the catalog `code` (e.g. `CONFIGURATION.BRANCH`). +1. **`moduleKey`** on `ModuleConfigEntity` equals the Admin catalog `code` (e.g. `ADMIN.SETTINGS.DATA.BRANCH`). 2. **Menu leaf** in `layouts/data/menu.data.ts` sets the same `moduleKey`. `filterMenuByViewPrivilege` hides the item when `ALLOW_VIEW` is false. 3. **Routes** wrap in `EnterpriseModuleProvider` so missing `ALLOW_VIEW` shows forbidden (no all-true flash). 4. **Do not** re-check create/edit/delete in page JSX. Foundations already hide actions from `PrivilegeEntity`. @@ -23,8 +23,8 @@ if (!user.permissions.BRANCHES?.create) return null; { key: 'branches', path: '/app/system/branches/index' } // GOOD -export const branchesModuleConfig = { moduleKey: 'CONFIGURATION.BRANCH', /* ... */ }; -{ key: 'system-branches', path: '/app/system/branches/index', moduleKey: 'CONFIGURATION.BRANCH' } +export const branchesModuleConfig = { moduleKey: 'ADMIN.SETTINGS.DATA.BRANCH', /* ... */ }; +{ key: 'system-branches', path: '/app/system/branches/index', moduleKey: 'ADMIN.SETTINGS.DATA.BRANCH' } ``` ## Flag map (`mapUserPrivileges`) @@ -37,6 +37,6 @@ export const branchesModuleConfig = { moduleKey: 'CONFIGURATION.BRANCH', /* ... | `delete` | `ALLOW_DELETE` | | `import` | `ALLOW_IMPORT` | -Missing flag → `false`. Cycles/plans: key follows `purpose` (`SALES.CYCLE` / `LOGISTICS.PLAN`), not a generic `CYCLES` key. +Missing flag → `false`. Cycles/plans: key follows `purpose` (`ADMIN.SALES.DATA.CYCLE` / `ADMIN.LOGISTICS.ACTIVITIES.PLAN`), not a generic `CYCLES` key. Reference: [apps/web/src/core/lib/map-user-privileges.ts](apps/web/src/core/lib/map-user-privileges.ts), [filter-menu-by-view-privilege.ts](apps/web/src/core/lib/filter-menu-by-view-privilege.ts). diff --git a/api.md b/api.md index ae2ced6..c85309a 100644 --- a/api.md +++ b/api.md @@ -240,36 +240,41 @@ HTTP mapping: | `DELETE /:id`, `POST /bulk-delete` | `delete` | | `POST /import` | `import` | -Catalog (`GET /privilege-keys`, needs `PRIVILEGES` `view`): +Catalog (`GET /privilege-keys`, needs `ADMIN.SETTINGS.USER.PRIVILEGES` `view`). Keys use `Group.Parent.Module` or `Group.Parent.Module.Submodule`: -| code | label | -| ------------------------ | ---------------- | -| `PRIVILEGES` | Privileges | -| `USERS` | Users | -| `CONFIGURATION.DIVISION` | Divisions | -| `CONFIGURATION.BRANCH` | Branches | -| `CONFIGURATION.CUSTOMER` | Customers | -| `CONFIGURATION.EMPLOYEE` | Employees | -| `CONFIGURATION.PRODUCT` | Products | -| `SALES.REQUEST` | Sales requests | -| `SALES.ORDER` | Sales orders | -| `SALES.PACKING_SLIP` | Packing slips | -| `SALES.INVOICE` | Sales invoices | -| `SALES.PAYMENT` | Sales payments | -| `CONFIGURATION.SETTING` | Company settings | -| `SALES.CYCLE` | Sales cycles | -| `SALES.PLAN` | Sales plans | -| `LOGISTICS.CYCLE` | Logistics cycles | -| `LOGISTICS.PLAN` | Logistics plans | +| code | label | +| ---- | ----- | +| `ADMIN.SETTINGS.USER.PRIVILEGES` | Privileges | +| `ADMIN.SETTINGS.USER.USERS` | Users | +| `ADMIN.SETTINGS.DATA.DIVISION` | Divisions | +| `ADMIN.SETTINGS.DATA.BRANCH` | Branches | +| `ADMIN.SETTINGS.DATA.CUSTOMER` | Customers | +| `ADMIN.SETTINGS.DATA.PRODUCT` | Products | +| `ADMIN.SETTINGS.DATA.SETTING` | Company settings | +| `ADMIN.SALES.DATA.EMPLOYEE` | Employees | +| `ADMIN.SALES.DATA.CYCLE` | Sales cycles | +| `ADMIN.SALES.ACTIVITIES.REQUEST` | Sales requests | +| `ADMIN.SALES.ACTIVITIES.ORDER` | Sales orders | +| `ADMIN.SALES.ACTIVITIES.INVOICE` | Sales invoices | +| `ADMIN.SALES.ACTIVITIES.PAYMENT` | Sales payments | +| `ADMIN.SALES.ACTIVITIES.PLAN` | Sales plans | +| `ADMIN.SALES.REPORT` | Sales reports | +| `ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP` | Packing slips | +| `ADMIN.LOGISTICS.DATA.CYCLE` | Logistics cycles | +| `ADMIN.LOGISTICS.ACTIVITIES.PLAN` | Logistics plans | +| `ADMIN.LOGISTICS.REPORT` | Logistics reports | +| `MOBILE.SALES.PLAN` | Sales plans (mobile) | +| `MOBILE.SALES.PLAN.ATTENDANCE` | Branch attendance | +| `MOBILE.SALES.VISIT` | Customer visits | ### Field purpose Cycles and plans do **not** use a single key. Privilege is resolved from `purpose`: -| purpose | cycle key | plan key | -| ----------- | ----------------- | ---------------- | -| `sales` | `SALES.CYCLE` | `SALES.PLAN` | -| `logistics` | `LOGISTICS.CYCLE` | `LOGISTICS.PLAN` | +| purpose | cycle keys | plan keys | +| ----------- | ---------- | --------- | +| `sales` | `ADMIN.SALES.DATA.CYCLE` | `ADMIN.SALES.ACTIVITIES.PLAN`, `MOBILE.SALES.PLAN` | +| `logistics` | `ADMIN.LOGISTICS.DATA.CYCLE` | `ADMIN.LOGISTICS.ACTIVITIES.PLAN`, `MOBILE.LOGISTICS.PLAN` | `purpose` is read from **body** (writes) or **query** (lists). If omitted, the user may proceed if they have the action on **either** purpose; list results are filtered to purposes they can view. Superadmin bypasses. diff --git a/apps/web/src/apps/auth/login/index.tsx b/apps/web/src/apps/auth/login/index.tsx index 573dfa3..3248d84 100644 --- a/apps/web/src/apps/auth/login/index.tsx +++ b/apps/web/src/apps/auth/login/index.tsx @@ -167,9 +167,6 @@ export default function LoginPage() { label: { color: '#64748b', fontWeight: 500 }, }} /> - - {t('forgot_password')} - - - - -
- - - -
diff --git a/apps/web/src/apps/main/layouts/data/menu.data.ts b/apps/web/src/apps/main/layouts/data/menu.data.ts index bf80d9c..bdaea77 100644 --- a/apps/web/src/apps/main/layouts/data/menu.data.ts +++ b/apps/web/src/apps/main/layouts/data/menu.data.ts @@ -50,14 +50,14 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:configuration-employees', icon: Users, path: '/app/sales/employees/index', - moduleKey: 'CONFIGURATION.EMPLOYEE', + moduleKey: 'ADMIN.SALES.DATA.EMPLOYEE', }, { key: 'sales-cycles', label: 'nav:sales-cycles', icon: Repeat, path: '/app/sales/cycles/index', - moduleKey: 'SALES.CYCLE', + moduleKey: 'ADMIN.SALES.DATA.CYCLE', }, ], }, @@ -72,35 +72,35 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:sales-requests', icon: ClipboardList, path: '/app/sales/requests/index', - moduleKey: 'SALES.REQUEST', + moduleKey: 'ADMIN.SALES.ACTIVITIES.REQUEST', }, { key: 'sales-orders', label: 'nav:sales-orders', icon: Box, path: '/app/sales/orders/index', - moduleKey: 'SALES.ORDER', + moduleKey: 'ADMIN.SALES.ACTIVITIES.ORDER', }, { key: 'sales-invoices', label: 'nav:sales-invoices', icon: Receipt, path: '/app/sales/invoices/index', - moduleKey: 'SALES.INVOICE', + moduleKey: 'ADMIN.SALES.ACTIVITIES.INVOICE', }, { key: 'sales-payments', label: 'nav:sales-payments', icon: CreditCard, path: '/app/sales/payments/index', - moduleKey: 'SALES.PAYMENT', + moduleKey: 'ADMIN.SALES.ACTIVITIES.PAYMENT', }, { key: 'sales-plans', label: 'nav:sales-plans', icon: Calendar, path: '/app/sales/plans/index', - moduleKey: 'SALES.PLAN', + moduleKey: 'ADMIN.SALES.ACTIVITIES.PLAN', }, ], }, @@ -109,7 +109,7 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:sales-reports', icon: FileText, path: '/app/sales/reports/index', - moduleKey: 'SALES.REPORT', + moduleKey: 'ADMIN.SALES.REPORT', }, ], }, @@ -130,14 +130,14 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:configuration-employees', icon: Users, path: '/app/logistics/employees/index', - moduleKey: 'CONFIGURATION.EMPLOYEE', + moduleKey: 'ADMIN.SALES.DATA.EMPLOYEE', }, { key: 'logistics-cycles', label: 'nav:logistics-cycles', icon: Repeat, path: '/app/logistics/cycles/index', - moduleKey: 'LOGISTICS.CYCLE', + moduleKey: 'ADMIN.LOGISTICS.DATA.CYCLE', }, ], }, @@ -152,14 +152,14 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:logistics-packing-slips', icon: Package, path: '/app/logistics/packing-slips/index', - moduleKey: 'SALES.PACKING_SLIP', + moduleKey: 'ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP', }, { key: 'logistics-plans', label: 'nav:logistics-plans', icon: Calendar, path: '/app/logistics/plans/index', - moduleKey: 'LOGISTICS.PLAN', + moduleKey: 'ADMIN.LOGISTICS.ACTIVITIES.PLAN', }, ], }, @@ -168,7 +168,7 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:logistics-reports', icon: FileText, path: '/app/logistics/reports/index', - moduleKey: 'LOGISTICS.REPORT', + moduleKey: 'ADMIN.LOGISTICS.REPORT', }, ], }, @@ -189,28 +189,28 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:configuration-branches', icon: MapPin, path: '/app/configuration/branches/index', - moduleKey: 'CONFIGURATION.BRANCH', + moduleKey: 'ADMIN.SETTINGS.DATA.BRANCH', }, { key: 'configuration-divisions', label: 'nav:configuration-divisions', icon: Layers, path: '/app/configuration/divisions/index', - moduleKey: 'CONFIGURATION.DIVISION', + moduleKey: 'ADMIN.SETTINGS.DATA.DIVISION', }, { key: 'configuration-customers', label: 'nav:configuration-customers', icon: Users, path: '/app/configuration/customers/index', - moduleKey: 'CONFIGURATION.CUSTOMER', + moduleKey: 'ADMIN.SETTINGS.DATA.CUSTOMER', }, { key: 'configuration-products', label: 'nav:configuration-products', icon: Package, path: '/app/configuration/products/index', - moduleKey: 'CONFIGURATION.PRODUCT', + moduleKey: 'ADMIN.SETTINGS.DATA.PRODUCT', }, ], }, @@ -225,14 +225,14 @@ export const MENU_ITEMS: MenuItemType[] = [ label: 'nav:system-users', icon: Users, path: '/app/system/users/index', - moduleKey: 'USERS', + moduleKey: 'ADMIN.SETTINGS.USER.USERS', }, { key: 'system-privileges', label: 'nav:system-privileges', icon: Shield, path: '/app/system/privileges/index', - moduleKey: 'PRIVILEGES', + moduleKey: 'ADMIN.SETTINGS.USER.PRIVILEGES', }, ], }, diff --git a/apps/web/src/apps/main/modules/configuration/branches/data/branch.remote.service.test.ts b/apps/web/src/apps/main/modules/configuration/branches/data/branch.remote.service.test.ts index be4fc75..fb54fa6 100644 --- a/apps/web/src/apps/main/modules/configuration/branches/data/branch.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/configuration/branches/data/branch.remote.service.test.ts @@ -33,7 +33,7 @@ describe('BranchesRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new BranchesRemoteDataServices(httpClient, { apiUrl: '/branches', - moduleKey: 'CONFIGURATION.BRANCH', + moduleKey: 'ADMIN.SETTINGS.DATA.BRANCH', transformer: new BranchesRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/configuration/branches/domain/constants/branch.constants.ts b/apps/web/src/apps/main/modules/configuration/branches/domain/constants/branch.constants.ts index c2b11db..08ba24e 100644 --- a/apps/web/src/apps/main/modules/configuration/branches/domain/constants/branch.constants.ts +++ b/apps/web/src/apps/main/modules/configuration/branches/domain/constants/branch.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { BranchEntity } from '../entities'; export const branchesModuleConfig: ModuleConfigEntity = { - moduleKey: 'CONFIGURATION.BRANCH', + moduleKey: 'ADMIN.SETTINGS.DATA.BRANCH', translationNamespace: 'BRANCHES', apiUrl: '/branches', webUrl: '/app/configuration/branches', diff --git a/apps/web/src/apps/main/modules/configuration/customers/data/customer.remote.service.test.ts b/apps/web/src/apps/main/modules/configuration/customers/data/customer.remote.service.test.ts index 730d916..1f055d7 100644 --- a/apps/web/src/apps/main/modules/configuration/customers/data/customer.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/configuration/customers/data/customer.remote.service.test.ts @@ -42,7 +42,7 @@ describe('CustomersRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new CustomersRemoteDataServices(httpClient, { apiUrl: '/customers', - moduleKey: 'CONFIGURATION.CUSTOMER', + moduleKey: 'ADMIN.SETTINGS.DATA.CUSTOMER', transformer: new CustomersRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/configuration/customers/domain/constants/customer.constants.ts b/apps/web/src/apps/main/modules/configuration/customers/domain/constants/customer.constants.ts index e1012f4..3d2637f 100644 --- a/apps/web/src/apps/main/modules/configuration/customers/domain/constants/customer.constants.ts +++ b/apps/web/src/apps/main/modules/configuration/customers/domain/constants/customer.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { CustomerEntity } from '../entities'; export const customersModuleConfig: ModuleConfigEntity = { - moduleKey: 'CONFIGURATION.CUSTOMER', + moduleKey: 'ADMIN.SETTINGS.DATA.CUSTOMER', translationNamespace: 'CUSTOMERS', apiUrl: '/customers', webUrl: '/app/configuration/customers', diff --git a/apps/web/src/apps/main/modules/configuration/divisions/data/division.remote.service.test.ts b/apps/web/src/apps/main/modules/configuration/divisions/data/division.remote.service.test.ts index 333ae67..45788fe 100644 --- a/apps/web/src/apps/main/modules/configuration/divisions/data/division.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/configuration/divisions/data/division.remote.service.test.ts @@ -44,7 +44,7 @@ describe('DivisionsRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new DivisionsRemoteDataServices(httpClient, { apiUrl: '/divisions', - moduleKey: 'CONFIGURATION.DIVISION', + moduleKey: 'ADMIN.SETTINGS.DATA.DIVISION', transformer: new DivisionsRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/configuration/divisions/domain/constants/division.constants.ts b/apps/web/src/apps/main/modules/configuration/divisions/domain/constants/division.constants.ts index 6a66ea0..882f34f 100644 --- a/apps/web/src/apps/main/modules/configuration/divisions/domain/constants/division.constants.ts +++ b/apps/web/src/apps/main/modules/configuration/divisions/domain/constants/division.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { DivisionEntity } from '../entities'; export const divisionsModuleConfig: ModuleConfigEntity = { - moduleKey: 'CONFIGURATION.DIVISION', + moduleKey: 'ADMIN.SETTINGS.DATA.DIVISION', translationNamespace: 'DIVISIONS', apiUrl: '/divisions', webUrl: '/app/configuration/divisions', diff --git a/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts b/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts index 6fff814..9a35418 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/data/employee.remote.service.test.ts @@ -33,7 +33,7 @@ describe('EmployeesRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new EmployeesRemoteDataServices(httpClient, { apiUrl: '/employees', - moduleKey: 'CONFIGURATION.EMPLOYEE', + moduleKey: 'ADMIN.SALES.DATA.EMPLOYEE', transformer: new EmployeesRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.test.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.test.ts index 3bab4cb..46fd2b4 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.test.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.test.ts @@ -14,7 +14,7 @@ describe('employee purpose helpers', () => { }); it('keeps a shared employee privilege key', () => { - expect(createEmployeeModuleConfig('sales').moduleKey).toBe('CONFIGURATION.EMPLOYEE'); - expect(createEmployeeModuleConfig('logistics').moduleKey).toBe('CONFIGURATION.EMPLOYEE'); + expect(createEmployeeModuleConfig('sales').moduleKey).toBe('ADMIN.SALES.DATA.EMPLOYEE'); + expect(createEmployeeModuleConfig('logistics').moduleKey).toBe('ADMIN.SALES.DATA.EMPLOYEE'); }); }); diff --git a/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts index 5a0438a..bcdace6 100644 --- a/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts +++ b/apps/web/src/apps/main/modules/configuration/employees/domain/constants/employee.constants.ts @@ -4,7 +4,7 @@ import type { EmployeeEntity } from '../entities'; export function createEmployeeModuleConfig(purpose: FieldPurpose): ModuleConfigEntity { return { - moduleKey: 'CONFIGURATION.EMPLOYEE', + moduleKey: 'ADMIN.SALES.DATA.EMPLOYEE', translationNamespace: 'EMPLOYEES', apiUrl: '/employees', webUrl: `/app/${purpose}/employees`, diff --git a/apps/web/src/apps/main/modules/configuration/products/data/product.remote.service.test.ts b/apps/web/src/apps/main/modules/configuration/products/data/product.remote.service.test.ts index b1f6961..c9d9302 100644 --- a/apps/web/src/apps/main/modules/configuration/products/data/product.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/configuration/products/data/product.remote.service.test.ts @@ -33,7 +33,7 @@ describe('ProductsRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new ProductsRemoteDataServices(httpClient, { apiUrl: '/products', - moduleKey: 'CONFIGURATION.PRODUCT', + moduleKey: 'ADMIN.SETTINGS.DATA.PRODUCT', transformer: new ProductsRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/configuration/products/domain/constants/product.constants.ts b/apps/web/src/apps/main/modules/configuration/products/domain/constants/product.constants.ts index b8b82a8..4558568 100644 --- a/apps/web/src/apps/main/modules/configuration/products/domain/constants/product.constants.ts +++ b/apps/web/src/apps/main/modules/configuration/products/domain/constants/product.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { ProductEntity } from '../entities'; export const productsModuleConfig: ModuleConfigEntity = { - moduleKey: 'CONFIGURATION.PRODUCT', + moduleKey: 'ADMIN.SETTINGS.DATA.PRODUCT', translationNamespace: 'PRODUCTS', apiUrl: '/products', webUrl: '/app/configuration/products', diff --git a/apps/web/src/apps/main/modules/field/cycles/data/cycle.remote.service.test.ts b/apps/web/src/apps/main/modules/field/cycles/data/cycle.remote.service.test.ts index a9f909e..20f348d 100644 --- a/apps/web/src/apps/main/modules/field/cycles/data/cycle.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/field/cycles/data/cycle.remote.service.test.ts @@ -33,7 +33,7 @@ describe('CyclesRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new CyclesRemoteDataServices(httpClient, { apiUrl: '/cycles', - moduleKey: 'SALES.CYCLE', + moduleKey: 'ADMIN.SALES.DATA.CYCLE', transformer: new CyclesRemoteDataTransformer('sales'), }); }); diff --git a/apps/web/src/apps/main/modules/field/cycles/domain/constants/cycle.constants.ts b/apps/web/src/apps/main/modules/field/cycles/domain/constants/cycle.constants.ts index e809998..d3a1480 100644 --- a/apps/web/src/apps/main/modules/field/cycles/domain/constants/cycle.constants.ts +++ b/apps/web/src/apps/main/modules/field/cycles/domain/constants/cycle.constants.ts @@ -5,7 +5,7 @@ import type { CycleEntity, CycleWeekdayRow } from '../entities'; export function createCycleModuleConfig(purpose: FieldPurpose): ModuleConfigEntity { return { - moduleKey: purpose === 'sales' ? 'SALES.CYCLE' : 'LOGISTICS.CYCLE', + moduleKey: purpose === 'sales' ? 'ADMIN.SALES.DATA.CYCLE' : 'ADMIN.LOGISTICS.DATA.CYCLE', translationNamespace: 'CYCLES', apiUrl: '/cycles', webUrl: `/app/${purpose}/cycles`, diff --git a/apps/web/src/apps/main/modules/field/logistics-reports/domain/constants/reports.constants.ts b/apps/web/src/apps/main/modules/field/logistics-reports/domain/constants/reports.constants.ts index 620d9e0..ff0c82f 100644 --- a/apps/web/src/apps/main/modules/field/logistics-reports/domain/constants/reports.constants.ts +++ b/apps/web/src/apps/main/modules/field/logistics-reports/domain/constants/reports.constants.ts @@ -4,7 +4,7 @@ import type { ModuleConfigEntity } from '@repo/ui/foundations'; export type ReportShellEntity = BaseEntity & { id: string }; export const logisticsReportsModuleConfig: ModuleConfigEntity = { - moduleKey: 'LOGISTICS.REPORT', + moduleKey: 'ADMIN.LOGISTICS.REPORT', translationNamespace: 'LOGISTICS_REPORTS', apiUrl: '/reports', webUrl: '/app/logistics/reports', diff --git a/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/packing-slip.constants.ts b/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/packing-slip.constants.ts index 60a27aa..a86e394 100644 --- a/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/packing-slip.constants.ts +++ b/apps/web/src/apps/main/modules/field/packing-slips/domain/constants/packing-slip.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { PackingSlipEntity } from '../entities'; export const packingSlipsModuleConfig: ModuleConfigEntity = { - moduleKey: 'SALES.PACKING_SLIP', + moduleKey: 'ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP', translationNamespace: 'PACKING_SLIPS', apiUrl: '/packing-slips', webUrl: '/app/logistics/packing-slips', diff --git a/apps/web/src/apps/main/modules/field/plans/data/plan.remote.service.test.ts b/apps/web/src/apps/main/modules/field/plans/data/plan.remote.service.test.ts index c323bdb..5a6bd75 100644 --- a/apps/web/src/apps/main/modules/field/plans/data/plan.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/field/plans/data/plan.remote.service.test.ts @@ -35,7 +35,7 @@ describe('PlansRemoteDataServices', () => { httpClient, { apiUrl: '/plans', - moduleKey: 'SALES.PLAN', + moduleKey: 'ADMIN.SALES.ACTIVITIES.PLAN', transformer: new PlansRemoteDataTransformer('sales'), }, 'sales', diff --git a/apps/web/src/apps/main/modules/field/plans/domain/constants/plan.constants.ts b/apps/web/src/apps/main/modules/field/plans/domain/constants/plan.constants.ts index 0b47f0d..6662404 100644 --- a/apps/web/src/apps/main/modules/field/plans/domain/constants/plan.constants.ts +++ b/apps/web/src/apps/main/modules/field/plans/domain/constants/plan.constants.ts @@ -4,7 +4,7 @@ import type { PlanEntity } from '../entities'; export function createPlanModuleConfig(purpose: FieldPurpose): ModuleConfigEntity { return { - moduleKey: purpose === 'sales' ? 'SALES.PLAN' : 'LOGISTICS.PLAN', + moduleKey: purpose === 'sales' ? 'ADMIN.SALES.ACTIVITIES.PLAN' : 'ADMIN.LOGISTICS.ACTIVITIES.PLAN', translationNamespace: 'PLANS', apiUrl: '/plans', webUrl: `/app/${purpose}/plans`, diff --git a/apps/web/src/apps/main/modules/field/shared/lookup.factories.ts b/apps/web/src/apps/main/modules/field/shared/lookup.factories.ts index 792e96a..8556944 100644 --- a/apps/web/src/apps/main/modules/field/shared/lookup.factories.ts +++ b/apps/web/src/apps/main/modules/field/shared/lookup.factories.ts @@ -5,12 +5,12 @@ import type { LookupEntity } from './lookup.entity'; export const salesInvoicesDataService = new LookupRemoteDataServices(apiClient, { apiUrl: '/sales-invoices', - moduleKey: 'SALES.INVOICE', + moduleKey: 'ADMIN.SALES.ACTIVITIES.INVOICE', }); export const packingSlipsDataService = new LookupRemoteDataServices(apiClient, { apiUrl: '/packing-slips', - moduleKey: 'SALES.PACKING_SLIP', + moduleKey: 'ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP', }); export const loadSalesInvoiceOptions = createOptionLoader((config) => diff --git a/apps/web/src/apps/main/modules/sales/invoices/data/sales-invoice.remote.service.test.ts b/apps/web/src/apps/main/modules/sales/invoices/data/sales-invoice.remote.service.test.ts index 03cb9b1..0ae44a8 100644 --- a/apps/web/src/apps/main/modules/sales/invoices/data/sales-invoice.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/sales/invoices/data/sales-invoice.remote.service.test.ts @@ -33,7 +33,7 @@ describe('SalesInvoicesRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new SalesInvoicesRemoteDataServices(httpClient, { apiUrl: '/sales-invoices', - moduleKey: 'SALES.INVOICE', + moduleKey: 'ADMIN.SALES.ACTIVITIES.INVOICE', transformer: new SalesInvoicesRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/sales/invoices/domain/constants/sales-invoice.constants.ts b/apps/web/src/apps/main/modules/sales/invoices/domain/constants/sales-invoice.constants.ts index 2810467..36821c8 100644 --- a/apps/web/src/apps/main/modules/sales/invoices/domain/constants/sales-invoice.constants.ts +++ b/apps/web/src/apps/main/modules/sales/invoices/domain/constants/sales-invoice.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { SalesInvoiceEntity } from '../entities'; export const salesInvoicesModuleConfig: ModuleConfigEntity = { - moduleKey: 'SALES.INVOICE', + moduleKey: 'ADMIN.SALES.ACTIVITIES.INVOICE', translationNamespace: 'SALES_INVOICES', apiUrl: '/sales-invoices', webUrl: '/app/sales/invoices', diff --git a/apps/web/src/apps/main/modules/sales/orders/data/sales-order.remote.service.test.ts b/apps/web/src/apps/main/modules/sales/orders/data/sales-order.remote.service.test.ts index 6855990..47533b2 100644 --- a/apps/web/src/apps/main/modules/sales/orders/data/sales-order.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/sales/orders/data/sales-order.remote.service.test.ts @@ -33,7 +33,7 @@ describe('SalesOrdersRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new SalesOrdersRemoteDataServices(httpClient, { apiUrl: '/sales-orders', - moduleKey: 'SALES.ORDER', + moduleKey: 'ADMIN.SALES.ACTIVITIES.ORDER', transformer: new SalesOrdersRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/sales/orders/domain/constants/sales-order.constants.ts b/apps/web/src/apps/main/modules/sales/orders/domain/constants/sales-order.constants.ts index a48c046..6a4a9e6 100644 --- a/apps/web/src/apps/main/modules/sales/orders/domain/constants/sales-order.constants.ts +++ b/apps/web/src/apps/main/modules/sales/orders/domain/constants/sales-order.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { SalesOrderEntity } from '../entities'; export const salesOrdersModuleConfig: ModuleConfigEntity = { - moduleKey: 'SALES.ORDER', + moduleKey: 'ADMIN.SALES.ACTIVITIES.ORDER', translationNamespace: 'SALES_ORDERS', apiUrl: '/sales-orders', webUrl: '/app/sales/orders', diff --git a/apps/web/src/apps/main/modules/sales/payments/data/sales-payment.remote.service.test.ts b/apps/web/src/apps/main/modules/sales/payments/data/sales-payment.remote.service.test.ts index 79a69e2..ed39339 100644 --- a/apps/web/src/apps/main/modules/sales/payments/data/sales-payment.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/sales/payments/data/sales-payment.remote.service.test.ts @@ -33,7 +33,7 @@ describe('SalesPaymentsRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new SalesPaymentsRemoteDataServices(httpClient, { apiUrl: '/sales-payments', - moduleKey: 'SALES.PAYMENT', + moduleKey: 'ADMIN.SALES.ACTIVITIES.PAYMENT', transformer: new SalesPaymentsRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/sales/payments/domain/constants/sales-payment.constants.ts b/apps/web/src/apps/main/modules/sales/payments/domain/constants/sales-payment.constants.ts index 6856178..6d04345 100644 --- a/apps/web/src/apps/main/modules/sales/payments/domain/constants/sales-payment.constants.ts +++ b/apps/web/src/apps/main/modules/sales/payments/domain/constants/sales-payment.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { SalesPaymentEntity } from '../entities'; export const salesPaymentsModuleConfig: ModuleConfigEntity = { - moduleKey: 'SALES.PAYMENT', + moduleKey: 'ADMIN.SALES.ACTIVITIES.PAYMENT', translationNamespace: 'SALES_PAYMENTS', apiUrl: '/sales-payments', webUrl: '/app/sales/payments', diff --git a/apps/web/src/apps/main/modules/sales/reports/domain/constants/reports.constants.ts b/apps/web/src/apps/main/modules/sales/reports/domain/constants/reports.constants.ts index af958c6..9230a2b 100644 --- a/apps/web/src/apps/main/modules/sales/reports/domain/constants/reports.constants.ts +++ b/apps/web/src/apps/main/modules/sales/reports/domain/constants/reports.constants.ts @@ -4,7 +4,7 @@ import type { ModuleConfigEntity } from '@repo/ui/foundations'; export type ReportShellEntity = BaseEntity & { id: string }; export const salesReportsModuleConfig: ModuleConfigEntity = { - moduleKey: 'SALES.REPORT', + moduleKey: 'ADMIN.SALES.REPORT', translationNamespace: 'SALES_REPORTS', apiUrl: '/reports', webUrl: '/app/sales/reports', diff --git a/apps/web/src/apps/main/modules/sales/requests/data/sales-request.remote.service.test.ts b/apps/web/src/apps/main/modules/sales/requests/data/sales-request.remote.service.test.ts index 409a6a1..797dc63 100644 --- a/apps/web/src/apps/main/modules/sales/requests/data/sales-request.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/sales/requests/data/sales-request.remote.service.test.ts @@ -33,7 +33,7 @@ describe('SalesRequestsRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new SalesRequestsRemoteDataServices(httpClient, { apiUrl: '/sales-requests', - moduleKey: 'SALES.REQUEST', + moduleKey: 'ADMIN.SALES.ACTIVITIES.REQUEST', transformer: new SalesRequestsRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/sales/requests/domain/constants/sales-request.constants.ts b/apps/web/src/apps/main/modules/sales/requests/domain/constants/sales-request.constants.ts index 2cfa58b..013f1da 100644 --- a/apps/web/src/apps/main/modules/sales/requests/domain/constants/sales-request.constants.ts +++ b/apps/web/src/apps/main/modules/sales/requests/domain/constants/sales-request.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { SalesRequestEntity } from '../entities'; export const salesRequestsModuleConfig: ModuleConfigEntity = { - moduleKey: 'SALES.REQUEST', + moduleKey: 'ADMIN.SALES.ACTIVITIES.REQUEST', translationNamespace: 'SALES_REQUESTS', apiUrl: '/sales-requests', webUrl: '/app/sales/requests', diff --git a/apps/web/src/apps/main/modules/sales/shared/sales-document.remote.service.test.ts b/apps/web/src/apps/main/modules/sales/shared/sales-document.remote.service.test.ts index 6c57fa9..2b9354e 100644 --- a/apps/web/src/apps/main/modules/sales/shared/sales-document.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/sales/shared/sales-document.remote.service.test.ts @@ -32,7 +32,7 @@ describe('SalesDocumentRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new SalesDocumentRemoteDataServices(httpClient, { apiUrl: '/sales-requests', - moduleKey: 'SALES.REQUEST', + moduleKey: 'ADMIN.SALES.ACTIVITIES.REQUEST', }); }); diff --git a/apps/web/src/apps/main/modules/system/privileges/data/privilege.remote.service.test.ts b/apps/web/src/apps/main/modules/system/privileges/data/privilege.remote.service.test.ts index 7cfdb69..1750cfb 100644 --- a/apps/web/src/apps/main/modules/system/privileges/data/privilege.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/system/privileges/data/privilege.remote.service.test.ts @@ -45,7 +45,7 @@ describe('PrivilegesRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new PrivilegesRemoteDataServices(httpClient, { apiUrl: '/privileges', - moduleKey: 'PRIVILEGES', + moduleKey: 'ADMIN.SETTINGS.USER.PRIVILEGES', transformer: new PrivilegesRemoteDataTransformer(), }); }); @@ -125,7 +125,7 @@ describe('PrivilegesRemoteDataServices', () => { it('lists privilege keys from /privilege-keys', async () => { vi.mocked(httpClient.request).mockResolvedValueOnce({ data: { - data: [{ id: 'key-1', code: 'SALES.INVOICE', label: 'Sales Invoice', sortOrder: 1 }], + data: [{ id: 'key-1', code: 'ADMIN.SALES.ACTIVITIES.INVOICE', label: 'Sales Invoice', sortOrder: 1 }], meta: { currentPage: 1, itemsPerPage: 200, totalItems: 1, totalPages: 1 }, }, status: 200, @@ -142,7 +142,7 @@ describe('PrivilegesRemoteDataServices', () => { ); expect(result.data[0]).toEqual({ id: 'key-1', - code: 'SALES.INVOICE', + code: 'ADMIN.SALES.ACTIVITIES.INVOICE', label: 'Sales Invoice', sortOrder: 1, }); diff --git a/apps/web/src/apps/main/modules/system/privileges/domain/constants/privilege.constants.ts b/apps/web/src/apps/main/modules/system/privileges/domain/constants/privilege.constants.ts index 426230a..8333755 100644 --- a/apps/web/src/apps/main/modules/system/privileges/domain/constants/privilege.constants.ts +++ b/apps/web/src/apps/main/modules/system/privileges/domain/constants/privilege.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { PrivilegeEntity } from '../entities'; export const privilegesModuleConfig: ModuleConfigEntity = { - moduleKey: 'PRIVILEGES', + moduleKey: 'ADMIN.SETTINGS.USER.PRIVILEGES', translationNamespace: 'PRIVILEGES', apiUrl: '/privileges', webUrl: '/app/system/privileges', diff --git a/apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.test.ts b/apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.test.ts new file mode 100644 index 0000000..cd44285 --- /dev/null +++ b/apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest'; +import { EMPTY_MATRIX_CELL } from './entities'; +import type { PrivilegeKeyEntity } from './entities'; +import { groupPrivilegeKeys, flattenParentSectionRows, parsePrivilegeKey } from './privilege-key-hierarchy'; + +const keys: PrivilegeKeyEntity[] = [ + { id: 'k1', code: 'ADMIN.SALES.DATA.CYCLE', label: 'Sales cycles', sortOrder: 1 }, + { id: 'k2', code: 'ADMIN.SALES.ACTIVITIES.PLAN', label: 'Sales plans', sortOrder: 2 }, + { id: 'k3', code: 'MOBILE.SALES.PLAN', label: 'Sales plans (mobile)', sortOrder: 3 }, + { id: 'k4', code: 'MOBILE.SALES.PLAN.ATTENDANCE', label: 'Branch attendance', sortOrder: 4 }, +]; + +function row(key: PrivilegeKeyEntity, view = false) { + return { + keyId: key.id, + code: key.code, + label: key.label, + sortOrder: key.sortOrder, + cell: { ...EMPTY_MATRIX_CELL, view }, + }; +} + +describe('parsePrivilegeKey', () => { + it('parses 3-part keys', () => { + expect(parsePrivilegeKey('MOBILE.SALES.PLAN')).toEqual({ + group: 'MOBILE', + parent: 'SALES', + module: 'PLAN', + submodule: null, + }); + }); + + it('parses 4-part keys', () => { + expect(parsePrivilegeKey('MOBILE.SALES.PLAN.ATTENDANCE')).toEqual({ + group: 'MOBILE', + parent: 'SALES', + module: 'PLAN', + submodule: 'ATTENDANCE', + }); + }); + + it('returns null for legacy keys', () => { + expect(parsePrivilegeKey('PRIVILEGES')).toBeNull(); + }); +}); + +describe('groupPrivilegeKeys', () => { + it('groups keys into tabs, parents, modules, and submodule rows', () => { + const rowsByKeyId = Object.fromEntries(keys.map((key) => [key.id, row(key)])); + + const grouped = groupPrivilegeKeys(keys, rowsByKeyId); + + expect(grouped.map((tab) => tab.group)).toEqual(['ADMIN', 'MOBILE']); + expect(grouped[0]?.parents[0]?.parent).toBe('SALES'); + expect(grouped[0]?.parents[0]?.modules.map((m) => m.module)).toEqual([ + 'ACTIVITIES', + 'DATA', + ]); + expect(grouped[1]?.parents[0]?.modules[0]?.directRow?.code).toBe('MOBILE.SALES.PLAN'); + expect(grouped[1]?.parents[0]?.modules[0]?.submodules[0]?.code).toBe( + 'MOBILE.SALES.PLAN.ATTENDANCE', + ); + }); +}); + +describe('flattenParentSectionRows', () => { + it('flattens modules and submodules into sortable table rows', () => { + const rowsByKeyId = Object.fromEntries(keys.map((key) => [key.id, row(key)])); + const grouped = groupPrivilegeKeys(keys, rowsByKeyId); + const salesParent = grouped[0]?.parents[0]; + + expect(salesParent).toBeDefined(); + const tableRows = flattenParentSectionRows(salesParent!); + + expect(tableRows.map((r) => r.code)).toEqual([ + 'ADMIN.SALES.DATA.CYCLE', + 'ADMIN.SALES.ACTIVITIES.PLAN', + ]); + expect(tableRows[0]).toMatchObject({ module: 'DATA', submodule: 'CYCLE' }); + expect(tableRows[1]).toMatchObject({ module: 'ACTIVITIES', submodule: 'PLAN' }); + }); +}); diff --git a/apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.ts b/apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.ts new file mode 100644 index 0000000..7e1371c --- /dev/null +++ b/apps/web/src/apps/main/modules/system/privileges/domain/privilege-key-hierarchy.ts @@ -0,0 +1,140 @@ +import type { PrivilegeKeyEntity, PrivilegeMatrixCell } from './entities'; + +export type ParsedPrivilegeKey = { + group: string; + parent: string; + module: string; + submodule: string | null; +}; + +export type PrivilegeMatrixRow = { + keyId: string; + code: string; + label: string; + sortOrder: number; + cell: PrivilegeMatrixCell; +}; + +export type PrivilegeModuleGroup = { + module: string; + directRow?: PrivilegeMatrixRow; + submodules: PrivilegeMatrixRow[]; +}; + +export type PrivilegeParentSection = { + parent: string; + modules: PrivilegeModuleGroup[]; +}; + +export type PrivilegeGroupTab = { + group: string; + parents: PrivilegeParentSection[]; +}; + +export function parsePrivilegeKey(code: string): ParsedPrivilegeKey | null { + const parts = code.split('.'); + if (parts.length === 3) { + const [group, parent, module] = parts; + return { group, parent, module, submodule: null }; + } + if (parts.length === 4) { + const [group, parent, module, submodule] = parts; + return { group, parent, module, submodule }; + } + return null; +} + +export function groupPrivilegeKeys( + keys: PrivilegeKeyEntity[], + rowsByKeyId: Record, +): PrivilegeGroupTab[] { + const groupMap = new Map>>(); + + const sortedKeys = [...keys].sort((a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code)); + + for (const key of sortedKeys) { + const parsed = parsePrivilegeKey(key.code); + if (!parsed) { + continue; + } + const row = rowsByKeyId[key.id]; + if (!row) { + continue; + } + + if (!groupMap.has(parsed.group)) { + groupMap.set(parsed.group, new Map()); + } + const parentMap = groupMap.get(parsed.group)!; + if (!parentMap.has(parsed.parent)) { + parentMap.set(parsed.parent, new Map()); + } + const moduleMap = parentMap.get(parsed.parent)!; + if (!moduleMap.has(parsed.module)) { + moduleMap.set(parsed.module, { module: parsed.module, submodules: [] }); + } + const moduleGroup = moduleMap.get(parsed.module)!; + + if (parsed.submodule === null) { + moduleGroup.directRow = row; + } else { + moduleGroup.submodules.push(row); + } + } + + return [...groupMap.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([group, parentMap]) => ({ + group, + parents: [...parentMap.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([parent, moduleMap]) => ({ + parent, + modules: [...moduleMap.values()] + .map((moduleGroup) => ({ + ...moduleGroup, + submodules: [...moduleGroup.submodules].sort( + (a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code), + ), + })) + .sort((a, b) => a.module.localeCompare(b.module)), + })), + })); +} + +export type PrivilegeTableRow = PrivilegeMatrixRow & { + module: string; + submodule: string | null; +}; + +export function flattenParentSectionRows(parent: PrivilegeParentSection): PrivilegeTableRow[] { + const rows: PrivilegeTableRow[] = []; + + for (const moduleGroup of parent.modules) { + if (moduleGroup.directRow) { + rows.push({ + ...moduleGroup.directRow, + module: moduleGroup.module, + submodule: null, + }); + } + + for (const subRow of moduleGroup.submodules) { + const parsed = parsePrivilegeKey(subRow.code); + rows.push({ + ...subRow, + module: moduleGroup.module, + submodule: parsed?.submodule ?? null, + }); + } + } + + return rows.sort((a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code)); +} + +export function formatPrivilegeSegment(segment: string): string { + return segment + .split('_') + .map((part) => part.charAt(0) + part.slice(1).toLowerCase()) + .join(' '); +} diff --git a/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts b/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts index 93da36c..7ef11c2 100644 --- a/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts +++ b/apps/web/src/apps/main/modules/system/privileges/domain/privilege.matrix-rows.test.ts @@ -17,7 +17,7 @@ describe('buildPrivilegeMatrixRows', () => { details: [ { privilegeKeyId: 'key-2', - keyCode: 'USERS', + keyCode: 'ADMIN.SETTINGS.USER.USERS', keyLabel: 'Users', sortOrder: 2, action: 'view', diff --git a/apps/web/src/apps/main/modules/system/privileges/domain/transformers/privilege.remote.transformer.test.ts b/apps/web/src/apps/main/modules/system/privileges/domain/transformers/privilege.remote.transformer.test.ts index b4b6893..7d60798 100644 --- a/apps/web/src/apps/main/modules/system/privileges/domain/transformers/privilege.remote.transformer.test.ts +++ b/apps/web/src/apps/main/modules/system/privileges/domain/transformers/privilege.remote.transformer.test.ts @@ -17,7 +17,7 @@ const detailDto: PrivilegeDetailResponseDto = { { id: 'd1', privilegeKeyId: 'key-1', - keyCode: 'SALES.INVOICE', + keyCode: 'ADMIN.SALES.ACTIVITIES.INVOICE', keyLabel: 'Sales Invoice', sortOrder: 1, action: 'view', @@ -26,7 +26,7 @@ const detailDto: PrivilegeDetailResponseDto = { { id: 'd2', privilegeKeyId: 'key-1', - keyCode: 'SALES.INVOICE', + keyCode: 'ADMIN.SALES.ACTIVITIES.INVOICE', keyLabel: 'Sales Invoice', sortOrder: 1, action: 'create', @@ -48,7 +48,7 @@ describe('PrivilegesRemoteDataTransformer', () => { }); expect(entity.details?.[0]).toMatchObject({ privilegeKeyId: 'key-1', - keyCode: 'SALES.INVOICE', + keyCode: 'ADMIN.SALES.ACTIVITIES.INVOICE', keyLabel: 'Sales Invoice', sortOrder: 1, }); diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/components/form-component/form-permissions.tsx b/apps/web/src/apps/main/modules/system/privileges/presentation/components/form-component/form-permissions.tsx index a490a68..87cd1ad 100644 --- a/apps/web/src/apps/main/modules/system/privileges/presentation/components/form-component/form-permissions.tsx +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/components/form-component/form-permissions.tsx @@ -1,9 +1,10 @@ -import { useEffect, useState } from 'react'; -import { Box, Paper, Table, Text } from '@repo/ui/components'; -import { FieldCheckbox } from '@repo/ui/form'; +import { useEffect, useMemo, useState } from 'react'; +import { Paper, Text } from '@repo/ui/components'; import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations'; import { privilegesDataService } from '../../../domain/factories'; -import { EMPTY_MATRIX_CELL, PRIVILEGE_ACTIONS, type PrivilegeKeyEntity } from '../../../domain/entities'; +import { EMPTY_MATRIX_CELL, type PrivilegeKeyEntity } from '../../../domain/entities'; +import { groupPrivilegeKeys } from '../../../domain/privilege-key-hierarchy'; +import { PrivilegeMatrixLayout } from '../privilege-matrix-layout'; export function FormPermissions() { const { formControl } = useFormPageContext(); @@ -28,42 +29,28 @@ export function FormPermissions() { }; }, [formControl]); + const grouped = useMemo(() => { + const rowsByKeyId = Object.fromEntries( + keys.map((key) => [ + key.id, + { + keyId: key.id, + code: key.code, + label: key.label, + sortOrder: key.sortOrder, + cell: formControl.getValues(`matrix.${key.id}`) ?? { ...EMPTY_MATRIX_CELL }, + }, + ]), + ); + return groupPrivilegeKeys(keys, rowsByKeyId); + }, [keys, formControl]); + return ( {t('section_permissions')} - - - - - {t('matrix_module')} - {PRIVILEGE_ACTIONS.map((action) => ( - - {t(`matrix_${action}`)} - - ))} - - - - {keys.map((key) => ( - - - {key.label || key.code} - - {key.code} - - - {PRIVILEGE_ACTIONS.map((action) => ( - - - - ))} - - ))} - -
-
+
); } diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx new file mode 100644 index 0000000..b1af7a0 --- /dev/null +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx @@ -0,0 +1,179 @@ +import { Box, Stack, Table, Tabs, Text } from '@repo/ui/components'; +import { FieldCheckbox } from '@repo/ui/form'; +import type { Control } from 'react-hook-form'; +import { Check, Minus } from 'lucide-react'; +import { PRIVILEGE_ACTIONS } from '../../domain/entities'; +import { + flattenParentSectionRows, + formatPrivilegeSegment, + type PrivilegeGroupTab, + type PrivilegeMatrixRow, + type PrivilegeTableRow, +} from '../../domain/privilege-key-hierarchy'; + +const MODULE_COL_WIDTH = '22%'; +const SUBMODULE_COL_WIDTH = '30%'; +const ACTION_COL_WIDTH = 88; + +type PrivilegeMatrixLayoutProps = { + groups: PrivilegeGroupTab[]; + t: (key: string) => string; + mode: 'edit' | 'read'; + formControl?: { control: Control> }; +}; + +function segmentLabel(t: (key: string) => string, kind: string, value: string) { + const key = `hierarchy_${kind}_${value.toLowerCase()}`; + const translated = t(key); + return translated === key ? formatPrivilegeSegment(value) : translated; +} + +function submoduleCellLabel( + t: (key: string) => string, + row: PrivilegeTableRow, +) { + if (row.submodule) { + return row.label || segmentLabel(t, 'submodule', row.submodule); + } + return row.label || segmentLabel(t, 'module', row.module); +} + +function MatrixActionCells({ + row, + mode, + formControl, +}: { + row: PrivilegeMatrixRow; + mode: 'edit' | 'read'; + formControl?: { control: Control> }; +}) { + return ( + <> + {PRIVILEGE_ACTIONS.map((action) => ( + + + {mode === 'edit' && formControl ? ( + + ) : row.cell[action] ? ( + + ) : ( + + )} + + + ))} + + ); +} + +function ParentMatrixTable({ + rows, + mode, + formControl, + t, +}: { + rows: PrivilegeTableRow[]; + mode: 'edit' | 'read'; + formControl?: { control: Control> }; + t: (key: string) => string; +}) { + if (rows.length === 0) { + return null; + } + + return ( + + + + + + {PRIVILEGE_ACTIONS.map((action) => ( + + ))} + + + + {t('matrix_module')} + {t('matrix_submodule')} + {PRIVILEGE_ACTIONS.map((action) => ( + + {t(`matrix_${action}`)} + + ))} + + + + {rows.map((row) => ( + + + + {segmentLabel(t, 'module', row.module)} + + + + {submoduleCellLabel(t, row)} + + {row.code} + + + + + ))} + +
+
+ ); +} + +export function PrivilegeMatrixLayout({ + groups, + t, + mode, + formControl, +}: PrivilegeMatrixLayoutProps) { + if (groups.length === 0) { + return null; + } + + return ( + + + {groups.map((groupTab) => ( + + {segmentLabel(t, 'group', groupTab.group)} + + ))} + + + {groups.map((groupTab) => ( + + + {groupTab.parents.map((parentSection) => ( + + {segmentLabel(t, 'parent', parentSection.parent)} + + + ))} + + + ))} + + ); +} diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx index 034282a..373ec42 100644 --- a/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-permissions-table.tsx @@ -1,7 +1,9 @@ -import { Box, Paper, Table, Text } from '@repo/ui/components'; -import { Check, Minus } from 'lucide-react'; +import { useMemo } from 'react'; +import { Paper, Text } from '@repo/ui/components'; import { PRIVILEGE_ACTIONS, type PrivilegeEntity } from '../../domain/entities'; import { buildPrivilegeMatrixRows } from '../../domain/privilege.matrix-rows'; +import { groupPrivilegeKeys } from '../../domain/privilege-key-hierarchy'; +import { PrivilegeMatrixLayout } from './privilege-matrix-layout'; export function PrivilegePermissionsTable({ privilege, @@ -10,44 +12,26 @@ export function PrivilegePermissionsTable({ privilege: Pick | null | undefined; t: (key: string) => string; }) { - const rows = buildPrivilegeMatrixRows(privilege); + const grouped = useMemo(() => { + const rows = buildPrivilegeMatrixRows(privilege); + const keys = rows.map((row) => ({ + id: row.keyId, + code: row.code, + label: row.label, + sortOrder: row.sortOrder, + })); + const rowsByKeyId = Object.fromEntries(rows.map((row) => [row.keyId, row])); + return groupPrivilegeKeys(keys, rowsByKeyId); + }, [privilege]); + + void PRIVILEGE_ACTIONS; return ( {t('section_permissions')} - - - - - {t('matrix_module')} - {PRIVILEGE_ACTIONS.map((action) => ( - - {t(`matrix_${action}`)} - - ))} - - - - {rows.map((row) => ( - - - {row.label} - - {row.code} - - - {PRIVILEGE_ACTIONS.map((action) => ( - - {row.cell[action] ? : } - - ))} - - ))} - -
-
+
); } diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/languages/en/privileges.json b/apps/web/src/apps/main/modules/system/privileges/presentation/languages/en/privileges.json index 2a155b0..687f370 100644 --- a/apps/web/src/apps/main/modules/system/privileges/presentation/languages/en/privileges.json +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/languages/en/privileges.json @@ -12,11 +12,22 @@ "section_general": "General", "section_permissions": "Permissions", "matrix_module": "Module", + "matrix_submodule": "Submodule", "matrix_view": "View", "matrix_create": "Create", "matrix_update": "Update", "matrix_delete": "Delete", "matrix_import": "Import", + "hierarchy_group_admin": "Admin", + "hierarchy_group_mobile": "Mobile", + "hierarchy_parent_sales": "Sales", + "hierarchy_parent_logistics": "Logistics", + "hierarchy_parent_settings": "Settings", + "hierarchy_module_data": "Data", + "hierarchy_module_activities": "Activities", + "hierarchy_module_report": "Report", + "hierarchy_module_user": "User", + "hierarchy_module_plan": "Plan", "status_draft": "Draft", "status_active": "Active", "status_archived": "Archived" diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/languages/id/privileges.json b/apps/web/src/apps/main/modules/system/privileges/presentation/languages/id/privileges.json index cfd80ab..11258fa 100644 --- a/apps/web/src/apps/main/modules/system/privileges/presentation/languages/id/privileges.json +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/languages/id/privileges.json @@ -12,11 +12,22 @@ "section_general": "Umum", "section_permissions": "Izin", "matrix_module": "Modul", + "matrix_submodule": "Submodul", "matrix_view": "Lihat", "matrix_create": "Buat", "matrix_update": "Ubah", "matrix_delete": "Hapus", "matrix_import": "Impor", + "hierarchy_group_admin": "Admin", + "hierarchy_group_mobile": "Mobile", + "hierarchy_parent_sales": "Penjualan", + "hierarchy_parent_logistics": "Logistik", + "hierarchy_parent_settings": "Pengaturan", + "hierarchy_module_data": "Data", + "hierarchy_module_activities": "Aktivitas", + "hierarchy_module_report": "Laporan", + "hierarchy_module_user": "Pengguna", + "hierarchy_module_plan": "Rencana", "status_draft": "Draft", "status_active": "Aktif", "status_archived": "Diarsipkan" diff --git a/apps/web/src/apps/main/modules/system/users/data/user.remote.service.test.ts b/apps/web/src/apps/main/modules/system/users/data/user.remote.service.test.ts index b16bd7e..8651e99 100644 --- a/apps/web/src/apps/main/modules/system/users/data/user.remote.service.test.ts +++ b/apps/web/src/apps/main/modules/system/users/data/user.remote.service.test.ts @@ -33,7 +33,7 @@ describe('UsersRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new UsersRemoteDataServices(httpClient, { apiUrl: '/users', - moduleKey: 'USERS', + moduleKey: 'ADMIN.SETTINGS.USER.USERS', transformer: new UsersRemoteDataTransformer(), }); }); diff --git a/apps/web/src/apps/main/modules/system/users/domain/constants/user.constants.ts b/apps/web/src/apps/main/modules/system/users/domain/constants/user.constants.ts index a187cb5..5323079 100644 --- a/apps/web/src/apps/main/modules/system/users/domain/constants/user.constants.ts +++ b/apps/web/src/apps/main/modules/system/users/domain/constants/user.constants.ts @@ -2,7 +2,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; import type { UserEntity } from '../entities'; export const usersModuleConfig: ModuleConfigEntity = { - moduleKey: 'USERS', + moduleKey: 'ADMIN.SETTINGS.USER.USERS', translationNamespace: 'USERS', apiUrl: '/users', webUrl: '/app/system/users', diff --git a/apps/web/src/core/constants/module-key.ts b/apps/web/src/core/constants/module-key.ts index 1cb9bdc..b5db688 100644 --- a/apps/web/src/core/constants/module-key.ts +++ b/apps/web/src/core/constants/module-key.ts @@ -1,12 +1,20 @@ export const MODULE_KEY = { - PRIVILEGES: 'PRIVILEGES', - USERS: 'USERS', - CONFIGURATION_DIVISION: 'CONFIGURATION.DIVISION', - CONFIGURATION_BRANCH: 'CONFIGURATION.BRANCH', - CONFIGURATION_CUSTOMER: 'CONFIGURATION.CUSTOMER', - CONFIGURATION_EMPLOYEE: 'CONFIGURATION.EMPLOYEE', - SALES_CYCLE: 'SALES.CYCLE', - SALES_PLAN: 'SALES.PLAN', - LOGISTICS_CYCLE: 'LOGISTICS.CYCLE', - LOGISTICS_PLAN: 'LOGISTICS.PLAN', + PRIVILEGES: 'ADMIN.SETTINGS.USER.PRIVILEGES', + USERS: 'ADMIN.SETTINGS.USER.USERS', + CONFIGURATION_DIVISION: 'ADMIN.SETTINGS.DATA.DIVISION', + CONFIGURATION_BRANCH: 'ADMIN.SETTINGS.DATA.BRANCH', + CONFIGURATION_CUSTOMER: 'ADMIN.SETTINGS.DATA.CUSTOMER', + CONFIGURATION_PRODUCT: 'ADMIN.SETTINGS.DATA.PRODUCT', + CONFIGURATION_EMPLOYEE: 'ADMIN.SALES.DATA.EMPLOYEE', + SALES_CYCLE: 'ADMIN.SALES.DATA.CYCLE', + SALES_REQUEST: 'ADMIN.SALES.ACTIVITIES.REQUEST', + SALES_ORDER: 'ADMIN.SALES.ACTIVITIES.ORDER', + SALES_INVOICE: 'ADMIN.SALES.ACTIVITIES.INVOICE', + SALES_PAYMENT: 'ADMIN.SALES.ACTIVITIES.PAYMENT', + SALES_PLAN: 'ADMIN.SALES.ACTIVITIES.PLAN', + SALES_REPORT: 'ADMIN.SALES.REPORT', + PACKING_SLIP: 'ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP', + LOGISTICS_CYCLE: 'ADMIN.LOGISTICS.DATA.CYCLE', + LOGISTICS_PLAN: 'ADMIN.LOGISTICS.ACTIVITIES.PLAN', + LOGISTICS_REPORT: 'ADMIN.LOGISTICS.REPORT', } as const; diff --git a/apps/web/src/core/lib/auth.helper.test.ts b/apps/web/src/core/lib/auth.helper.test.ts index cdec7f8..8d7124c 100644 --- a/apps/web/src/core/lib/auth.helper.test.ts +++ b/apps/web/src/core/lib/auth.helper.test.ts @@ -38,7 +38,7 @@ const me = { isSuperadmin: false, privilege: { id: 'priv-1', name: 'Admin', code: 'ADMIN' }, permissions: { - PRIVILEGES: { view: true, create: false, update: false, delete: false }, + 'ADMIN.SETTINGS.USER.PRIVILEGES': { view: true, create: false, update: false, delete: false }, }, }; @@ -83,7 +83,7 @@ describe('auth.helper', () => { expect(appDatabase.setItem).toHaveBeenCalledWith( 'user_privilege', expect.objectContaining({ - PRIVILEGES: expect.objectContaining({ ALLOW_VIEW: true, ALLOW_CREATE: false }), + 'ADMIN.SETTINGS.USER.PRIVILEGES': expect.objectContaining({ ALLOW_VIEW: true, ALLOW_CREATE: false }), }), ); expect(replace).toHaveBeenCalledWith('/app'); diff --git a/apps/web/src/core/lib/enterprise-storage-adapter.test.ts b/apps/web/src/core/lib/enterprise-storage-adapter.test.ts index f11980e..04570ad 100644 --- a/apps/web/src/core/lib/enterprise-storage-adapter.test.ts +++ b/apps/web/src/core/lib/enterprise-storage-adapter.test.ts @@ -27,7 +27,7 @@ describe('enterpriseStorageAdapter', () => { return null; }); - await expect(enterpriseStorageAdapter.getPrivileges('PRIVILEGES')).resolves.toEqual(defaultPrivileges); + await expect(enterpriseStorageAdapter.getPrivileges('ADMIN.SETTINGS.USER.PRIVILEGES')).resolves.toEqual(defaultPrivileges); await expect(enterpriseStorageAdapter.getPrivileges('UNKNOWN')).resolves.toEqual(defaultPrivileges); }); @@ -35,12 +35,12 @@ describe('enterpriseStorageAdapter', () => { vi.mocked(appDatabase.getItem).mockImplementation(async (key) => { if (key === 'user_profile') return { isSuperadmin: false }; if (key === 'user_privilege') { - return { PRIVILEGES: { ...noPrivileges, ALLOW_VIEW: true } }; + return { 'ADMIN.SETTINGS.USER.PRIVILEGES': { ...noPrivileges, ALLOW_VIEW: true } }; } return null; }); - await expect(enterpriseStorageAdapter.getPrivileges('PRIVILEGES')).resolves.toEqual({ + await expect(enterpriseStorageAdapter.getPrivileges('ADMIN.SETTINGS.USER.PRIVILEGES')).resolves.toEqual({ ...noPrivileges, ALLOW_VIEW: true, }); diff --git a/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts b/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts index 47987d3..a75d825 100644 --- a/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts +++ b/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts @@ -14,7 +14,7 @@ const items: TestMenuItem[] = [ { key: 'system', children: [ - { key: 'privileges', moduleKey: 'PRIVILEGES', path: '/app/system/privileges/index' }, + { key: 'privileges', moduleKey: 'ADMIN.SETTINGS.USER.PRIVILEGES', path: '/app/system/privileges/index' }, { key: 'placeholder', path: '/app/system/other' }, ], }, @@ -34,7 +34,7 @@ describe('filterMenuByViewPrivilege', () => { const filtered = filterMenuByViewPrivilege( items, { - PRIVILEGES: { ...noPrivileges, ALLOW_VIEW: false }, + 'ADMIN.SETTINGS.USER.PRIVILEGES': { ...noPrivileges, ALLOW_VIEW: false }, EXAMPLE_FULL_PAGE: { ...noPrivileges, ALLOW_VIEW: true }, }, false, @@ -50,7 +50,7 @@ describe('filterMenuByViewPrivilege', () => { const filtered = filterMenuByViewPrivilege( items, { - PRIVILEGES: { ...noPrivileges }, + 'ADMIN.SETTINGS.USER.PRIVILEGES': { ...noPrivileges }, EXAMPLE_FULL_PAGE: { ...noPrivileges }, }, false, @@ -72,13 +72,13 @@ describe('filterMenuByViewPrivilege', () => { { key: 'sales', children: [ - { key: 'orders', moduleKey: 'SALES.ORDER', path: '/app/sales/orders/index' }, + { key: 'orders', moduleKey: 'ADMIN.SALES.ACTIVITIES.ORDER', path: '/app/sales/orders/index' }, { key: 'reports', path: '/app/sales/reports', isPlaceholder: true }, ], }, ]; - const filtered = filterMenuByViewPrivilege(menu, { 'SALES.ORDER': { ...noPrivileges } }, false); + const filtered = filterMenuByViewPrivilege(menu, { 'ADMIN.SALES.ACTIVITIES.ORDER': { ...noPrivileges } }, false); expect(filtered.find((item) => item.key === 'sales')).toBeUndefined(); }); @@ -88,13 +88,13 @@ describe('filterMenuByViewPrivilege', () => { { key: 'sales', children: [ - { key: 'orders', moduleKey: 'SALES.ORDER', path: '/app/sales/orders/index' }, + { key: 'orders', moduleKey: 'ADMIN.SALES.ACTIVITIES.ORDER', path: '/app/sales/orders/index' }, { key: 'reports', path: '/app/sales/reports', isPlaceholder: true }, ], }, ]; - const filtered = filterMenuByViewPrivilege(menu, { 'SALES.ORDER': { ...noPrivileges, ALLOW_VIEW: true } }, false); + const filtered = filterMenuByViewPrivilege(menu, { 'ADMIN.SALES.ACTIVITIES.ORDER': { ...noPrivileges, ALLOW_VIEW: true } }, false); expect(filtered.find((item) => item.key === 'sales')?.children?.map((child) => child.key)).toEqual([ 'orders', diff --git a/apps/web/src/core/lib/map-user-privileges.test.ts b/apps/web/src/core/lib/map-user-privileges.test.ts index 2061375..a032a3b 100644 --- a/apps/web/src/core/lib/map-user-privileges.test.ts +++ b/apps/web/src/core/lib/map-user-privileges.test.ts @@ -6,7 +6,7 @@ describe('mapUserPrivileges', () => { it('maps view/create/update/delete/import flags', () => { const result = mapUserPrivileges( { - PRIVILEGES: { + 'ADMIN.SETTINGS.USER.PRIVILEGES': { view: true, create: true, update: false, @@ -17,7 +17,7 @@ describe('mapUserPrivileges', () => { false, ); - expect(result.PRIVILEGES).toEqual({ + expect(result['ADMIN.SETTINGS.USER.PRIVILEGES']).toEqual({ ...noPrivileges, ALLOW_VIEW: true, ALLOW_CREATE: true, @@ -28,9 +28,9 @@ describe('mapUserPrivileges', () => { }); it('defaults missing ALLOW flags to false', () => { - const result = mapUserPrivileges({ CUSTOMERS: { view: true } }, false); + const result = mapUserPrivileges({ 'ADMIN.SETTINGS.DATA.CUSTOMER': { view: true } }, false); - expect(result.CUSTOMERS).toEqual({ + expect(result['ADMIN.SETTINGS.DATA.CUSTOMER']).toEqual({ ...noPrivileges, ALLOW_VIEW: true, ALLOW_CREATE: false, @@ -42,7 +42,7 @@ describe('mapUserPrivileges', () => { it('returns an empty map for superadmin (adapter grants full privileges)', () => { const result = mapUserPrivileges( { - PRIVILEGES: { view: true, create: true, update: true, delete: true }, + 'ADMIN.SETTINGS.USER.PRIVILEGES': { view: true, create: true, update: true, delete: true }, }, true, ); diff --git a/apps/web/src/core/lib/trackgo-remote-data-services.test.ts b/apps/web/src/core/lib/trackgo-remote-data-services.test.ts index 663e613..0e4b786 100644 --- a/apps/web/src/core/lib/trackgo-remote-data-services.test.ts +++ b/apps/web/src/core/lib/trackgo-remote-data-services.test.ts @@ -53,7 +53,7 @@ describe('TrackGoRemoteDataServices', () => { httpClient = createMockHttpClient(); service = new SampleRemoteDataServices(httpClient, { apiUrl: '/divisions', - moduleKey: 'CONFIGURATION.DIVISION', + moduleKey: 'ADMIN.SETTINGS.DATA.DIVISION', transformer: new SampleTransformer(), }); });