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(),
});
});