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 }];