diff --git a/apps/web/src/apps/index.tsx b/apps/web/src/apps/index.tsx index e5d1f68..101d80d 100644 --- a/apps/web/src/apps/index.tsx +++ b/apps/web/src/apps/index.tsx @@ -4,6 +4,7 @@ import { ThemeProvider } from '@repo/ui/provider'; import { StatusPage, AgGridProvider } from '@repo/ui/components'; import { useTranslation } from '@repo/core-i18n'; import { LoadingScreen } from '../core/components/loading-screen'; +import { ComingSoonPage } from '../core/components/coming-soon-page'; import { useThemeStore } from '../core/stores/theme.store'; import { initializeAndPurgeHistoryBackground } from './main/layouts/hooks/useHistoryTracker'; @@ -53,17 +54,6 @@ function MaintenancePage() { ); } -function ComingSoonPage() { - const { t } = useTranslation(); - return ( - - ); -} - const router = createBrowserRouter([ { path: '/auth/*', element: }, { path: '/app/*', element: }, diff --git a/apps/web/src/apps/main/layouts/data/menu.data.test.ts b/apps/web/src/apps/main/layouts/data/menu.data.test.ts new file mode 100644 index 0000000..db28f99 --- /dev/null +++ b/apps/web/src/apps/main/layouts/data/menu.data.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; +import navEn from '../languages/en/nav.json'; +import navId from '../languages/id/nav.json'; +import { MENU_ITEMS } from './menu.data'; +import type { MenuItemType } from '../types/menu.types'; + +const childKeys = (item: MenuItemType | undefined): string[] => (item?.children ?? []).map((child) => child.key); + +const findItem = (items: MenuItemType[], key: string): MenuItemType | undefined => items.find((item) => item.key === key); + +const flatten = (items: MenuItemType[]): MenuItemType[] => + items.flatMap((item) => [item, ...(item.children ? flatten(item.children) : [])]); + +describe('MENU_ITEMS', () => { + it('orders top-level items as dashboard, sales, logistics, settings', () => { + expect(MENU_ITEMS.map((item) => item.key)).toEqual(['dashboard', 'sales', 'logistics', 'settings']); + }); + + it('nests sales as data, activities, then reports', () => { + const sales = findItem(MENU_ITEMS, 'sales'); + + expect(childKeys(sales)).toEqual(['sales-data', 'sales-activities', 'sales-reports']); + expect(childKeys(findItem(sales?.children ?? [], 'sales-data'))).toEqual(['sales-employees', 'sales-cycles']); + expect(childKeys(findItem(sales?.children ?? [], 'sales-activities'))).toEqual([ + 'sales-requests', + 'sales-orders', + 'sales-invoices', + 'sales-payments', + 'sales-plans', + ]); + }); + + it('nests logistics as data, activities, then reports', () => { + const logistics = findItem(MENU_ITEMS, 'logistics'); + + expect(childKeys(logistics)).toEqual(['logistics-data', 'logistics-activities', 'logistics-reports']); + expect(childKeys(findItem(logistics?.children ?? [], 'logistics-data'))).toEqual([ + 'logistics-employees', + 'logistics-cycles', + ]); + expect(childKeys(findItem(logistics?.children ?? [], 'logistics-activities'))).toEqual([ + 'logistics-packing-slips', + 'logistics-plans', + ]); + }); + + it('nests settings as data then user', () => { + const settings = findItem(MENU_ITEMS, 'settings'); + + expect(childKeys(settings)).toEqual(['settings-data', 'settings-user']); + expect(childKeys(findItem(settings?.children ?? [], 'settings-data'))).toEqual([ + 'configuration-branches', + 'configuration-divisions', + 'configuration-customers', + 'configuration-products', + ]); + expect(childKeys(findItem(settings?.children ?? [], 'settings-user'))).toEqual(['system-users', 'system-privileges']); + }); + + it('uses unique keys across the whole tree', () => { + const keys = flatten(MENU_ITEMS).map((item) => item.key); + expect(new Set(keys).size).toBe(keys.length); + }); + + it('resolves every label in both locales', () => { + for (const item of flatten(MENU_ITEMS)) { + const key = item.label.replace('nav:', ''); + expect(navEn).toHaveProperty(key); + expect(navId).toHaveProperty(key); + } + }); +}); 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 61b1e57..a235059 100644 --- a/apps/web/src/apps/main/layouts/data/menu.data.ts +++ b/apps/web/src/apps/main/layouts/data/menu.data.ts @@ -7,24 +7,13 @@ import { Users, Box, Layers, - Warehouse, - Building2, MapPin, Activity, - Globe, - Briefcase, - Phone, ShoppingCart, Truck, - HardHat, - Factory, - Calculator, Receipt, - PiggyBank, Calendar, - Clock, Shield, - FileSearch, Repeat, Package, ClipboardList, @@ -44,77 +33,83 @@ export const MENU_ITEMS: MenuItemType[] = [ icon: LayoutDashboard, path: '/app/dashboard', }, - // { - // key: 'crm', - // label: 'nav:crm', - // icon: Users, - // path: '/app/crm', - // children: [ - // { - // key: 'crm-leads', - // label: 'nav:crm-leads', - // icon: Briefcase, - // path: '/app/crm/leads', - // }, - // { - // key: 'crm-pipelines', - // label: 'nav:crm-pipelines', - // icon: Activity, - // path: '/app/crm/pipelines', - // }, - // { - // key: 'crm-contacts', - // label: 'nav:crm-contacts', - // icon: Phone, - // path: '/app/crm/contacts', - // }, - // ], - // }, { key: 'sales', label: 'nav:sales', icon: ShoppingCart, path: '/app/sales', children: [ - // { - // key: 'sales-quotations', - // label: 'nav:sales-quotations', - // icon: FileText, - // path: '/app/sales/quotations', - // }, { - key: 'sales-requests', - label: 'nav:sales-requests', - icon: ClipboardList, - path: '/app/sales/requests/index', - moduleKey: 'SALES.REQUEST', + key: 'sales-data', + label: 'nav:data', + icon: Database, + path: '/app/sales/data', + children: [ + { + key: 'sales-employees', + label: 'nav:configuration-employees', + icon: Users, + path: '/app/configuration/employees/index', + moduleKey: 'CONFIGURATION.EMPLOYEE', + }, + { + key: 'sales-cycles', + label: 'nav:sales-cycles', + icon: Repeat, + path: '/app/sales/cycles/index', + moduleKey: 'SALES.CYCLE', + }, + ], }, { - key: 'sales-orders', - label: 'nav:sales-orders', - icon: Box, - path: '/app/sales/orders/index', - moduleKey: 'SALES.ORDER', + key: 'sales-activities', + label: 'nav:activities', + icon: Activity, + path: '/app/sales/activities', + children: [ + { + key: 'sales-requests', + label: 'nav:sales-requests', + icon: ClipboardList, + path: '/app/sales/requests/index', + moduleKey: 'SALES.REQUEST', + }, + { + key: 'sales-orders', + label: 'nav:sales-orders', + icon: Box, + path: '/app/sales/orders/index', + moduleKey: 'SALES.ORDER', + }, + { + key: 'sales-invoices', + label: 'nav:sales-invoices', + icon: Receipt, + path: '/app/sales/invoices', + moduleKey: 'SALES.INVOICE', + }, + { + key: 'sales-payments', + label: 'nav:sales-payments', + icon: CreditCard, + path: '/app/sales/payments', + moduleKey: 'SALES.PAYMENT', + }, + { + key: 'sales-plans', + label: 'nav:sales-plans', + icon: Calendar, + path: '/app/sales/plans/index', + moduleKey: 'SALES.PLAN', + }, + ], }, { - key: 'sales-invoices', - label: 'nav:sales-invoices', - icon: Receipt, - path: '/app/sales/invoices', - }, - { - key: 'sales-cycles', - label: 'nav:sales-cycles', - icon: Repeat, - path: '/app/sales/cycles/index', - moduleKey: 'SALES.CYCLE', - }, - { - key: 'sales-plans', - label: 'nav:sales-plans', - icon: Calendar, - path: '/app/sales/plans/index', - moduleKey: 'SALES.PLAN', + key: 'sales-reports', + label: 'nav:reports-coming-soon', + icon: FileText, + path: '/app/sales/reports', + isPlaceholder: true, }, ], }, @@ -125,249 +120,122 @@ export const MENU_ITEMS: MenuItemType[] = [ path: '/app/logistics', children: [ { - key: 'logistics-cycles', - label: 'nav:logistics-cycles', - icon: Repeat, - path: '/app/logistics/cycles/index', - moduleKey: 'LOGISTICS.CYCLE', + key: 'logistics-data', + label: 'nav:data', + icon: Database, + path: '/app/logistics/data', + children: [ + { + key: 'logistics-employees', + label: 'nav:configuration-employees', + icon: Users, + path: '/app/configuration/employees/index', + moduleKey: 'CONFIGURATION.EMPLOYEE', + }, + { + key: 'logistics-cycles', + label: 'nav:logistics-cycles', + icon: Repeat, + path: '/app/logistics/cycles/index', + moduleKey: 'LOGISTICS.CYCLE', + }, + ], }, { - key: 'logistics-plans', - label: 'nav:logistics-plans', - icon: Calendar, - path: '/app/logistics/plans/index', - moduleKey: 'LOGISTICS.PLAN', - }, - ], - }, - // { - // key: 'supply-chain', - // label: 'nav:supply-chain', - // icon: Truck, - // path: '/app/supply-chain', - // children: [ - // { - // key: 'sc-inventory', - // label: 'nav:sc-inventory', - // icon: Box, - // path: '/app/supply-chain/inventory', - // children: [ - // { - // key: 'sc-inventory-products', - // label: 'nav:sc-inventory-products', - // icon: Layers, - // path: '/app/supply-chain/inventory/products', - // }, - // { - // key: 'sc-inventory-categories', - // label: 'nav:sc-inventory-categories', - // icon: Globe, - // path: '/app/supply-chain/inventory/categories', - // }, - // { - // key: 'sc-inventory-adjustments', - // label: 'nav:sc-inventory-adjustments', - // icon: FileSearch, - // path: '/app/supply-chain/inventory/adjustments', - // }, - // ], - // }, - // { - // key: 'sc-warehouses', - // label: 'nav:sc-warehouses', - // icon: Warehouse, - // path: '/app/supply-chain/warehouses', - // }, - // { - // key: 'sc-logistics', - // label: 'nav:sc-logistics', - // icon: Globe, - // path: '/app/supply-chain/logistics', - // }, - // ], - // }, - // { - // key: 'manufacturing', - // label: 'nav:manufacturing', - // icon: Factory, - // path: '/app/manufacturing', - // children: [ - // { - // key: 'mfg-bom', - // label: 'nav:mfg-bom', - // icon: Layers, - // path: '/app/manufacturing/bom', - // }, - // { - // key: 'mfg-work-orders', - // label: 'nav:mfg-work-orders', - // icon: HardHat, - // path: '/app/manufacturing/work-orders', - // }, - // ], - // }, - // { - // key: 'hris', - // label: 'nav:hris', - // icon: Briefcase, - // path: '/app/hris', - // children: [ - // { - // key: 'hris-employees', - // label: 'nav:hris-employees', - // icon: Users, - // path: '/app/hris/employees', - // }, - // { - // key: 'hris-attendance', - // label: 'nav:hris-attendance', - // icon: Clock, - // path: '/app/hris/attendance', - // }, - // { - // key: 'hris-payroll', - // label: 'nav:hris-payroll', - // icon: CreditCard, - // path: '/app/hris/payroll', - // }, - // { - // key: 'hris-calendar', - // label: 'nav:hris-calendar', - // icon: Calendar, - // path: '/app/hris/calendar', - // }, - // ], - // }, - // { - // key: 'accounting', - // label: 'nav:accounting', - // icon: Calculator, - // path: '/app/accounting', - // children: [ - // { - // key: 'acc-gl', - // label: 'nav:acc-gl', - // icon: Database, - // path: '/app/accounting/general-ledger', - // }, - // { - // key: 'acc-taxes', - // label: 'nav:acc-taxes', - // icon: PiggyBank, - // path: '/app/accounting/taxes', - // }, - // ], - // }, - // { - // key: 'settings', - // label: 'nav:settings', - // icon: Settings, - // path: '/app/settings', - // children: [ - // { - // key: 'settings-general', - // label: 'nav:settings-general', - // icon: Settings, - // path: '/app/settings/general', - // }, - // { - // key: 'settings-security', - // label: 'nav:settings-security', - // icon: Shield, - // path: '/app/settings/security', - // }, - // { - // key: 'long-text-2', - // label: 'nav:long-text-2', - // icon: FileText, - // path: '/app/settings/long-menu-test', - // }, - // ], - // }, - { - key: 'configuration', - label: 'nav:configuration', - icon: Building2, - path: '/app/configuration', - children: [ - { - key: 'configuration-divisions', - label: 'nav:configuration-divisions', - icon: Layers, - path: '/app/configuration/divisions/index', - moduleKey: 'CONFIGURATION.DIVISION', + key: 'logistics-activities', + label: 'nav:activities', + icon: Activity, + path: '/app/logistics/activities', + children: [ + { + key: 'logistics-packing-slips', + label: 'nav:logistics-packing-slips', + icon: Package, + path: '/app/logistics/packing-slips', + moduleKey: 'SALES.PACKING_SLIP', + }, + { + key: 'logistics-plans', + label: 'nav:logistics-plans', + icon: Calendar, + path: '/app/logistics/plans/index', + moduleKey: 'LOGISTICS.PLAN', + }, + ], }, { - key: 'configuration-branches', - label: 'nav:configuration-branches', - icon: MapPin, - path: '/app/configuration/branches/index', - moduleKey: 'CONFIGURATION.BRANCH', - }, - { - key: 'configuration-customers', - label: 'nav:configuration-customers', - icon: Users, - path: '/app/configuration/customers/index', - moduleKey: 'CONFIGURATION.CUSTOMER', - }, - { - key: 'configuration-employees', - label: 'nav:configuration-employees', - icon: Users, - path: '/app/configuration/employees/index', - moduleKey: 'CONFIGURATION.EMPLOYEE', - }, - { - key: 'configuration-products', - label: 'nav:configuration-products', - icon: Package, - path: '/app/configuration/products/index', - moduleKey: 'CONFIGURATION.PRODUCT', + key: 'logistics-reports', + label: 'nav:reports-coming-soon', + icon: FileText, + path: '/app/logistics/reports', + isPlaceholder: true, }, ], }, { - key: 'system-group', - label: 'nav:system', - icon: Shield, - path: '/app/system', + key: 'settings', + label: 'nav:settings', + icon: Settings, + path: '/app/settings', children: [ { - key: 'system-privileges', - label: 'nav:system-privileges', - icon: Shield, - path: '/app/system/privileges/index', - moduleKey: 'PRIVILEGES', + key: 'settings-data', + label: 'nav:data', + icon: Database, + path: '/app/settings/data', + children: [ + { + key: 'configuration-branches', + label: 'nav:configuration-branches', + icon: MapPin, + path: '/app/configuration/branches/index', + moduleKey: 'CONFIGURATION.BRANCH', + }, + { + key: 'configuration-divisions', + label: 'nav:configuration-divisions', + icon: Layers, + path: '/app/configuration/divisions/index', + moduleKey: 'CONFIGURATION.DIVISION', + }, + { + key: 'configuration-customers', + label: 'nav:configuration-customers', + icon: Users, + path: '/app/configuration/customers/index', + moduleKey: 'CONFIGURATION.CUSTOMER', + }, + { + key: 'configuration-products', + label: 'nav:configuration-products', + icon: Package, + path: '/app/configuration/products/index', + moduleKey: 'CONFIGURATION.PRODUCT', + }, + ], }, { - key: 'system-users', - label: 'nav:system-users', + key: 'settings-user', + label: 'nav:user', icon: Users, - path: '/app/system/users/index', - moduleKey: 'USERS', + path: '/app/settings/user', + children: [ + { + key: 'system-users', + label: 'nav:system-users', + icon: Users, + path: '/app/system/users/index', + moduleKey: 'USERS', + }, + { + key: 'system-privileges', + label: 'nav:system-privileges', + icon: Shield, + path: '/app/system/privileges/index', + moduleKey: 'PRIVILEGES', + }, + ], }, ], }, - // { - // key: 'example-module', - // label: 'nav:example-module', - // icon: Database, - // path: '/app/example-module', - // children: [ - // { - // key: 'example-full-page', - // label: 'nav:example-full-page', - // icon: LayoutDashboard, - // path: '/app/example/full-page/index', - // moduleKey: 'EXAMPLE_FULL_PAGE', - // }, - // // { - // // key: 'example-single-page', - // // label: 'nav:example-single-page', - // // icon: FileText, - // // path: '/app/example/single-page/index', - // // }, - // ], - // }, ]; diff --git a/apps/web/src/apps/main/layouts/languages/en/nav.json b/apps/web/src/apps/main/layouts/languages/en/nav.json index 0a0047d..f98db11 100644 --- a/apps/web/src/apps/main/layouts/languages/en/nav.json +++ b/apps/web/src/apps/main/layouts/languages/en/nav.json @@ -8,7 +8,8 @@ "sales-quotations": "Quotations", "sales-requests": "Sales Requests", "sales-orders": "Sales Orders", - "sales-invoices": "Invoices", + "sales-invoices": "Sales Invoices", + "sales-payments": "Sales Payments", "supply-chain": "Supply Chain", "sc-inventory": "Inventory Management", "sc-inventory-products": "Products", @@ -27,7 +28,11 @@ "accounting": "Accounting", "acc-gl": "General Ledger", "acc-taxes": "Taxes", - "settings": "Settings & Configuration", + "data": "Data", + "activities": "Activities", + "reports-coming-soon": "Reports (Coming Soon)", + "user": "User", + "settings": "Settings", "settings-general": "General Settings", "settings-security": "Security", "long-text-2": "Extremely Long Menu Name To Test Text Truncation Handling Properly", @@ -46,6 +51,7 @@ "logistics": "Logistics", "logistics-cycles": "Logistics Cycles", "logistics-plans": "Logistics Plans", + "logistics-packing-slips": "Packing Slips", "configuration-employees": "Employees", "configuration-products": "Products" } diff --git a/apps/web/src/apps/main/layouts/languages/id/nav.json b/apps/web/src/apps/main/layouts/languages/id/nav.json index af189d4..b6a91f9 100644 --- a/apps/web/src/apps/main/layouts/languages/id/nav.json +++ b/apps/web/src/apps/main/layouts/languages/id/nav.json @@ -8,7 +8,8 @@ "sales-quotations": "Penawaran", "sales-requests": "Permintaan Penjualan", "sales-orders": "Pesanan Penjualan", - "sales-invoices": "Faktur", + "sales-invoices": "Faktur Penjualan", + "sales-payments": "Pembayaran Penjualan", "supply-chain": "Rantai Pasok", "sc-inventory": "Manajemen Inventaris", "sc-inventory-products": "Produk", @@ -27,7 +28,11 @@ "accounting": "Akuntansi", "acc-gl": "Buku Besar", "acc-taxes": "Pajak", - "settings": "Pengaturan & Konfigurasi", + "data": "Data", + "activities": "Aktivitas", + "reports-coming-soon": "Laporan (Segera Hadir)", + "user": "Pengguna", + "settings": "Pengaturan", "settings-general": "Pengaturan Umum", "settings-security": "Keamanan", "long-text-2": "Nama Menu Sangat Panjang Untuk Menguji Penanganan Pemotongan Teks Dengan Baik", @@ -46,6 +51,7 @@ "logistics": "Logistik", "logistics-cycles": "Siklus Logistik", "logistics-plans": "Rencana Logistik", + "logistics-packing-slips": "Surat Jalan", "configuration-employees": "Karyawan", "configuration-products": "Produk" } diff --git a/apps/web/src/apps/main/layouts/types/menu.types.ts b/apps/web/src/apps/main/layouts/types/menu.types.ts index 8bb9e2d..83e5c40 100644 --- a/apps/web/src/apps/main/layouts/types/menu.types.ts +++ b/apps/web/src/apps/main/layouts/types/menu.types.ts @@ -16,4 +16,6 @@ export interface MenuItemType { children?: MenuItemType[]; /** Privilege catalog key used to hide the item when ALLOW_VIEW is false */ moduleKey?: string; + /** Coming-soon leaf that must not keep an otherwise-empty parent visible */ + isPlaceholder?: boolean; } diff --git a/apps/web/src/apps/main/modules/field/logistics/index.tsx b/apps/web/src/apps/main/modules/field/logistics/index.tsx index fae816d..21ebc51 100644 --- a/apps/web/src/apps/main/modules/field/logistics/index.tsx +++ b/apps/web/src/apps/main/modules/field/logistics/index.tsx @@ -1,5 +1,6 @@ import { lazy } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; +import { EmbeddedComingSoonPage } from '../../../../../core/components/coming-soon-page'; const CyclesModule = lazy(() => import('../cycles/presentation/factory')); const PlansModule = lazy(() => import('../plans/presentation/factory')); @@ -9,6 +10,8 @@ export default function LogisticsFieldModule() { } /> } /> + } /> + } /> } /> ); diff --git a/apps/web/src/apps/main/modules/sales/index.tsx b/apps/web/src/apps/main/modules/sales/index.tsx index d1cb2f7..02bfcbf 100644 --- a/apps/web/src/apps/main/modules/sales/index.tsx +++ b/apps/web/src/apps/main/modules/sales/index.tsx @@ -1,5 +1,6 @@ import { lazy } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; +import { EmbeddedComingSoonPage } from '../../../../core/components/coming-soon-page'; const RequestsModule = lazy(() => import('./requests/presentation/factory')); const OrdersModule = lazy(() => import('./orders/presentation/factory')); @@ -13,6 +14,9 @@ export default function SalesModule() { } /> } /> } /> + } /> + } /> + } /> } /> ); diff --git a/apps/web/src/core/components/coming-soon-page.tsx b/apps/web/src/core/components/coming-soon-page.tsx new file mode 100644 index 0000000..ca1fdbf --- /dev/null +++ b/apps/web/src/core/components/coming-soon-page.tsx @@ -0,0 +1,29 @@ +import { StatusPage } from '@repo/ui/components'; +import { useTranslation } from '@repo/core-i18n'; + +interface ComingSoonPageProps { + /** Use inside ModuleLayout so the page fits under the app chrome */ + embedded?: boolean; +} + +export function ComingSoonPage({ embedded = false }: ComingSoonPageProps) { + const { t } = useTranslation(); + + return ( + + ); +} + +export function EmbeddedComingSoonPage() { + return ; +} 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 fe4546a..6b31a57 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 @@ -2,7 +2,15 @@ import { describe, expect, it } from 'vitest'; import { noPrivileges } from '@repo/ui/foundations'; import { filterMenuByViewPrivilege } from './filter-menu-by-view-privilege'; -const items = [ +type TestMenuItem = { + key: string; + path?: string; + moduleKey?: string; + isPlaceholder?: boolean; + children?: TestMenuItem[]; +}; + +const items: TestMenuItem[] = [ { key: 'system', children: [ @@ -58,4 +66,43 @@ describe('filterMenuByViewPrivilege', () => { const filtered = filterMenuByViewPrivilege(items, {}, false); expect(filtered.find((item) => item.key === 'dashboard')).toEqual({ key: 'dashboard', path: '/app/dashboard' }); }); + + it('drops a parent when the only remaining children are placeholders', () => { + const menu: TestMenuItem[] = [ + { + key: 'sales', + children: [ + { key: 'orders', moduleKey: 'SALES.ORDER', path: '/app/sales/orders/index' }, + { key: 'reports', path: '/app/sales/reports', isPlaceholder: true }, + ], + }, + ]; + + const filtered = filterMenuByViewPrivilege(menu, { 'SALES.ORDER': { ...noPrivileges } }, false); + + expect(filtered.find((item) => item.key === 'sales')).toBeUndefined(); + }); + + it('keeps placeholders when a real sibling remains', () => { + const menu: TestMenuItem[] = [ + { + key: 'sales', + children: [ + { key: 'orders', moduleKey: 'SALES.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, + ); + + expect(filtered.find((item) => item.key === 'sales')?.children?.map((child) => child.key)).toEqual([ + 'orders', + 'reports', + ]); + }); }); diff --git a/apps/web/src/core/lib/filter-menu-by-view-privilege.ts b/apps/web/src/core/lib/filter-menu-by-view-privilege.ts index 77c952d..89afdec 100644 --- a/apps/web/src/core/lib/filter-menu-by-view-privilege.ts +++ b/apps/web/src/core/lib/filter-menu-by-view-privilege.ts @@ -1,10 +1,8 @@ import type { PrivilegeEntity } from '@repo/ui/foundations'; -export function filterMenuByViewPrivilege( - items: T[], - privileges: Record, - isSuperadmin: boolean, -): T[] { +export function filterMenuByViewPrivilege< + T extends { moduleKey?: string; children?: T[]; isPlaceholder?: boolean }, +>(items: T[], privileges: Record, isSuperadmin: boolean): T[] { if (isSuperadmin) { return items; } @@ -17,7 +15,8 @@ export function filterMenuByViewPrivilege !child.isPlaceholder); + if (!hasRealChild) { return []; } return [{ ...item, children }];