feat: implement coming soon pages and enhance menu structure
- Added a new ComingSoonPage component for displaying a status page for features that are not yet available. - Integrated the ComingSoonPage into the sales and logistics modules for routes such as invoices, payments, packing slips, and reports. - Updated the MENU_ITEMS structure to include new nested items for sales and logistics, enhancing the organization of the menu. - Introduced placeholder functionality in the menu to manage visibility of items that are not yet available. - Updated navigation labels in both English and Indonesian to reflect new menu items and improve user experience. These changes enhance the application's navigation and user experience by clearly indicating upcoming features and improving menu organization.
This commit is contained in:
@@ -4,6 +4,7 @@ import { ThemeProvider } from '@repo/ui/provider';
|
|||||||
import { StatusPage, AgGridProvider } from '@repo/ui/components';
|
import { StatusPage, AgGridProvider } from '@repo/ui/components';
|
||||||
import { useTranslation } from '@repo/core-i18n';
|
import { useTranslation } from '@repo/core-i18n';
|
||||||
import { LoadingScreen } from '../core/components/loading-screen';
|
import { LoadingScreen } from '../core/components/loading-screen';
|
||||||
|
import { ComingSoonPage } from '../core/components/coming-soon-page';
|
||||||
import { useThemeStore } from '../core/stores/theme.store';
|
import { useThemeStore } from '../core/stores/theme.store';
|
||||||
import { initializeAndPurgeHistoryBackground } from './main/layouts/hooks/useHistoryTracker';
|
import { initializeAndPurgeHistoryBackground } from './main/layouts/hooks/useHistoryTracker';
|
||||||
|
|
||||||
@@ -53,17 +54,6 @@ function MaintenancePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComingSoonPage() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<StatusPage
|
|
||||||
title={t('common:systemPages.comingSoon.title')}
|
|
||||||
heading={t('common:systemPages.comingSoon.heading')}
|
|
||||||
description={t('common:systemPages.comingSoon.description')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: '/auth/*', element: <AuthModule /> },
|
{ path: '/auth/*', element: <AuthModule /> },
|
||||||
{ path: '/app/*', element: <AppModule /> },
|
{ path: '/app/*', element: <AppModule /> },
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,24 +7,13 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
Box,
|
Box,
|
||||||
Layers,
|
Layers,
|
||||||
Warehouse,
|
|
||||||
Building2,
|
|
||||||
MapPin,
|
MapPin,
|
||||||
Activity,
|
Activity,
|
||||||
Globe,
|
|
||||||
Briefcase,
|
|
||||||
Phone,
|
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Truck,
|
Truck,
|
||||||
HardHat,
|
|
||||||
Factory,
|
|
||||||
Calculator,
|
|
||||||
Receipt,
|
Receipt,
|
||||||
PiggyBank,
|
|
||||||
Calendar,
|
Calendar,
|
||||||
Clock,
|
|
||||||
Shield,
|
Shield,
|
||||||
FileSearch,
|
|
||||||
Repeat,
|
Repeat,
|
||||||
Package,
|
Package,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
@@ -44,77 +33,83 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
path: '/app/dashboard',
|
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',
|
key: 'sales',
|
||||||
label: 'nav:sales',
|
label: 'nav:sales',
|
||||||
icon: ShoppingCart,
|
icon: ShoppingCart,
|
||||||
path: '/app/sales',
|
path: '/app/sales',
|
||||||
children: [
|
children: [
|
||||||
// {
|
|
||||||
// key: 'sales-quotations',
|
|
||||||
// label: 'nav:sales-quotations',
|
|
||||||
// icon: FileText,
|
|
||||||
// path: '/app/sales/quotations',
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: 'sales-requests',
|
key: 'sales-data',
|
||||||
label: 'nav:sales-requests',
|
label: 'nav:data',
|
||||||
icon: ClipboardList,
|
icon: Database,
|
||||||
path: '/app/sales/requests/index',
|
path: '/app/sales/data',
|
||||||
moduleKey: 'SALES.REQUEST',
|
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',
|
key: 'sales-activities',
|
||||||
label: 'nav:sales-orders',
|
label: 'nav:activities',
|
||||||
icon: Box,
|
icon: Activity,
|
||||||
path: '/app/sales/orders/index',
|
path: '/app/sales/activities',
|
||||||
moduleKey: 'SALES.ORDER',
|
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',
|
key: 'sales-reports',
|
||||||
label: 'nav:sales-invoices',
|
label: 'nav:reports-coming-soon',
|
||||||
icon: Receipt,
|
icon: FileText,
|
||||||
path: '/app/sales/invoices',
|
path: '/app/sales/reports',
|
||||||
},
|
isPlaceholder: true,
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -125,249 +120,122 @@ export const MENU_ITEMS: MenuItemType[] = [
|
|||||||
path: '/app/logistics',
|
path: '/app/logistics',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'logistics-cycles',
|
key: 'logistics-data',
|
||||||
label: 'nav:logistics-cycles',
|
label: 'nav:data',
|
||||||
icon: Repeat,
|
icon: Database,
|
||||||
path: '/app/logistics/cycles/index',
|
path: '/app/logistics/data',
|
||||||
moduleKey: 'LOGISTICS.CYCLE',
|
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',
|
key: 'logistics-activities',
|
||||||
label: 'nav:logistics-plans',
|
label: 'nav:activities',
|
||||||
icon: Calendar,
|
icon: Activity,
|
||||||
path: '/app/logistics/plans/index',
|
path: '/app/logistics/activities',
|
||||||
moduleKey: 'LOGISTICS.PLAN',
|
children: [
|
||||||
},
|
{
|
||||||
],
|
key: 'logistics-packing-slips',
|
||||||
},
|
label: 'nav:logistics-packing-slips',
|
||||||
// {
|
icon: Package,
|
||||||
// key: 'supply-chain',
|
path: '/app/logistics/packing-slips',
|
||||||
// label: 'nav:supply-chain',
|
moduleKey: 'SALES.PACKING_SLIP',
|
||||||
// icon: Truck,
|
},
|
||||||
// path: '/app/supply-chain',
|
{
|
||||||
// children: [
|
key: 'logistics-plans',
|
||||||
// {
|
label: 'nav:logistics-plans',
|
||||||
// key: 'sc-inventory',
|
icon: Calendar,
|
||||||
// label: 'nav:sc-inventory',
|
path: '/app/logistics/plans/index',
|
||||||
// icon: Box,
|
moduleKey: 'LOGISTICS.PLAN',
|
||||||
// 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: 'configuration-branches',
|
key: 'logistics-reports',
|
||||||
label: 'nav:configuration-branches',
|
label: 'nav:reports-coming-soon',
|
||||||
icon: MapPin,
|
icon: FileText,
|
||||||
path: '/app/configuration/branches/index',
|
path: '/app/logistics/reports',
|
||||||
moduleKey: 'CONFIGURATION.BRANCH',
|
isPlaceholder: true,
|
||||||
},
|
|
||||||
{
|
|
||||||
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: 'system-group',
|
key: 'settings',
|
||||||
label: 'nav:system',
|
label: 'nav:settings',
|
||||||
icon: Shield,
|
icon: Settings,
|
||||||
path: '/app/system',
|
path: '/app/settings',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: 'system-privileges',
|
key: 'settings-data',
|
||||||
label: 'nav:system-privileges',
|
label: 'nav:data',
|
||||||
icon: Shield,
|
icon: Database,
|
||||||
path: '/app/system/privileges/index',
|
path: '/app/settings/data',
|
||||||
moduleKey: 'PRIVILEGES',
|
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',
|
key: 'settings-user',
|
||||||
label: 'nav:system-users',
|
label: 'nav:user',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: '/app/system/users/index',
|
path: '/app/settings/user',
|
||||||
moduleKey: 'USERS',
|
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',
|
|
||||||
// // },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sales-quotations": "Quotations",
|
"sales-quotations": "Quotations",
|
||||||
"sales-requests": "Sales Requests",
|
"sales-requests": "Sales Requests",
|
||||||
"sales-orders": "Sales Orders",
|
"sales-orders": "Sales Orders",
|
||||||
"sales-invoices": "Invoices",
|
"sales-invoices": "Sales Invoices",
|
||||||
|
"sales-payments": "Sales Payments",
|
||||||
"supply-chain": "Supply Chain",
|
"supply-chain": "Supply Chain",
|
||||||
"sc-inventory": "Inventory Management",
|
"sc-inventory": "Inventory Management",
|
||||||
"sc-inventory-products": "Products",
|
"sc-inventory-products": "Products",
|
||||||
@@ -27,7 +28,11 @@
|
|||||||
"accounting": "Accounting",
|
"accounting": "Accounting",
|
||||||
"acc-gl": "General Ledger",
|
"acc-gl": "General Ledger",
|
||||||
"acc-taxes": "Taxes",
|
"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-general": "General Settings",
|
||||||
"settings-security": "Security",
|
"settings-security": "Security",
|
||||||
"long-text-2": "Extremely Long Menu Name To Test Text Truncation Handling Properly",
|
"long-text-2": "Extremely Long Menu Name To Test Text Truncation Handling Properly",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"logistics": "Logistics",
|
"logistics": "Logistics",
|
||||||
"logistics-cycles": "Logistics Cycles",
|
"logistics-cycles": "Logistics Cycles",
|
||||||
"logistics-plans": "Logistics Plans",
|
"logistics-plans": "Logistics Plans",
|
||||||
|
"logistics-packing-slips": "Packing Slips",
|
||||||
"configuration-employees": "Employees",
|
"configuration-employees": "Employees",
|
||||||
"configuration-products": "Products"
|
"configuration-products": "Products"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"sales-quotations": "Penawaran",
|
"sales-quotations": "Penawaran",
|
||||||
"sales-requests": "Permintaan Penjualan",
|
"sales-requests": "Permintaan Penjualan",
|
||||||
"sales-orders": "Pesanan Penjualan",
|
"sales-orders": "Pesanan Penjualan",
|
||||||
"sales-invoices": "Faktur",
|
"sales-invoices": "Faktur Penjualan",
|
||||||
|
"sales-payments": "Pembayaran Penjualan",
|
||||||
"supply-chain": "Rantai Pasok",
|
"supply-chain": "Rantai Pasok",
|
||||||
"sc-inventory": "Manajemen Inventaris",
|
"sc-inventory": "Manajemen Inventaris",
|
||||||
"sc-inventory-products": "Produk",
|
"sc-inventory-products": "Produk",
|
||||||
@@ -27,7 +28,11 @@
|
|||||||
"accounting": "Akuntansi",
|
"accounting": "Akuntansi",
|
||||||
"acc-gl": "Buku Besar",
|
"acc-gl": "Buku Besar",
|
||||||
"acc-taxes": "Pajak",
|
"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-general": "Pengaturan Umum",
|
||||||
"settings-security": "Keamanan",
|
"settings-security": "Keamanan",
|
||||||
"long-text-2": "Nama Menu Sangat Panjang Untuk Menguji Penanganan Pemotongan Teks Dengan Baik",
|
"long-text-2": "Nama Menu Sangat Panjang Untuk Menguji Penanganan Pemotongan Teks Dengan Baik",
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"logistics": "Logistik",
|
"logistics": "Logistik",
|
||||||
"logistics-cycles": "Siklus Logistik",
|
"logistics-cycles": "Siklus Logistik",
|
||||||
"logistics-plans": "Rencana Logistik",
|
"logistics-plans": "Rencana Logistik",
|
||||||
|
"logistics-packing-slips": "Surat Jalan",
|
||||||
"configuration-employees": "Karyawan",
|
"configuration-employees": "Karyawan",
|
||||||
"configuration-products": "Produk"
|
"configuration-products": "Produk"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,6 @@ export interface MenuItemType {
|
|||||||
children?: MenuItemType[];
|
children?: MenuItemType[];
|
||||||
/** Privilege catalog key used to hide the item when ALLOW_VIEW is false */
|
/** Privilege catalog key used to hide the item when ALLOW_VIEW is false */
|
||||||
moduleKey?: string;
|
moduleKey?: string;
|
||||||
|
/** Coming-soon leaf that must not keep an otherwise-empty parent visible */
|
||||||
|
isPlaceholder?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
import { EmbeddedComingSoonPage } from '../../../../../core/components/coming-soon-page';
|
||||||
|
|
||||||
const CyclesModule = lazy(() => import('../cycles/presentation/factory'));
|
const CyclesModule = lazy(() => import('../cycles/presentation/factory'));
|
||||||
const PlansModule = lazy(() => import('../plans/presentation/factory'));
|
const PlansModule = lazy(() => import('../plans/presentation/factory'));
|
||||||
@@ -9,6 +10,8 @@ export default function LogisticsFieldModule() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/cycles/*" element={<CyclesModule purpose="logistics" />} />
|
<Route path="/cycles/*" element={<CyclesModule purpose="logistics" />} />
|
||||||
<Route path="/plans/*" element={<PlansModule purpose="logistics" />} />
|
<Route path="/plans/*" element={<PlansModule purpose="logistics" />} />
|
||||||
|
<Route path="/packing-slips" element={<EmbeddedComingSoonPage />} />
|
||||||
|
<Route path="/reports" element={<EmbeddedComingSoonPage />} />
|
||||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
import { EmbeddedComingSoonPage } from '../../../../core/components/coming-soon-page';
|
||||||
|
|
||||||
const RequestsModule = lazy(() => import('./requests/presentation/factory'));
|
const RequestsModule = lazy(() => import('./requests/presentation/factory'));
|
||||||
const OrdersModule = lazy(() => import('./orders/presentation/factory'));
|
const OrdersModule = lazy(() => import('./orders/presentation/factory'));
|
||||||
@@ -13,6 +14,9 @@ export default function SalesModule() {
|
|||||||
<Route path="/orders/*" element={<OrdersModule />} />
|
<Route path="/orders/*" element={<OrdersModule />} />
|
||||||
<Route path="/cycles/*" element={<CyclesModule purpose="sales" />} />
|
<Route path="/cycles/*" element={<CyclesModule purpose="sales" />} />
|
||||||
<Route path="/plans/*" element={<PlansModule purpose="sales" />} />
|
<Route path="/plans/*" element={<PlansModule purpose="sales" />} />
|
||||||
|
<Route path="/invoices" element={<EmbeddedComingSoonPage />} />
|
||||||
|
<Route path="/payments" element={<EmbeddedComingSoonPage />} />
|
||||||
|
<Route path="/reports" element={<EmbeddedComingSoonPage />} />
|
||||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<StatusPage
|
||||||
|
title={t('common:systemPages.comingSoon.title')}
|
||||||
|
heading={t('common:systemPages.comingSoon.heading')}
|
||||||
|
description={t('common:systemPages.comingSoon.description')}
|
||||||
|
showActionsBack={!embedded}
|
||||||
|
showActionsHome
|
||||||
|
homeUrl="/app"
|
||||||
|
backButtonLabel={t('common:systemPages.actions.goBack')}
|
||||||
|
homeButtonLabel={t('common:systemPages.actions.backToHome')}
|
||||||
|
height={embedded ? 500 : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmbeddedComingSoonPage() {
|
||||||
|
return <ComingSoonPage embedded />;
|
||||||
|
}
|
||||||
@@ -2,7 +2,15 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { noPrivileges } from '@repo/ui/foundations';
|
import { noPrivileges } from '@repo/ui/foundations';
|
||||||
import { filterMenuByViewPrivilege } from './filter-menu-by-view-privilege';
|
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',
|
key: 'system',
|
||||||
children: [
|
children: [
|
||||||
@@ -58,4 +66,43 @@ describe('filterMenuByViewPrivilege', () => {
|
|||||||
const filtered = filterMenuByViewPrivilege(items, {}, false);
|
const filtered = filterMenuByViewPrivilege(items, {}, false);
|
||||||
expect(filtered.find((item) => item.key === 'dashboard')).toEqual({ key: 'dashboard', path: '/app/dashboard' });
|
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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { PrivilegeEntity } from '@repo/ui/foundations';
|
import type { PrivilegeEntity } from '@repo/ui/foundations';
|
||||||
|
|
||||||
export function filterMenuByViewPrivilege<T extends { moduleKey?: string; children?: T[] }>(
|
export function filterMenuByViewPrivilege<
|
||||||
items: T[],
|
T extends { moduleKey?: string; children?: T[]; isPlaceholder?: boolean },
|
||||||
privileges: Record<string, PrivilegeEntity>,
|
>(items: T[], privileges: Record<string, PrivilegeEntity>, isSuperadmin: boolean): T[] {
|
||||||
isSuperadmin: boolean,
|
|
||||||
): T[] {
|
|
||||||
if (isSuperadmin) {
|
if (isSuperadmin) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -17,7 +15,8 @@ export function filterMenuByViewPrivilege<T extends { moduleKey?: string; childr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
if (children.length === 0) {
|
const hasRealChild = children.some((child) => !child.isPlaceholder);
|
||||||
|
if (!hasRealChild) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [{ ...item, children }];
|
return [{ ...item, children }];
|
||||||
|
|||||||
Reference in New Issue
Block a user