diff --git a/frontend/src/components/AsideMenuItem.tsx b/frontend/src/components/AsideMenuItem.tsx index dbb09b2..8f494c3 100644 --- a/frontend/src/components/AsideMenuItem.tsx +++ b/frontend/src/components/AsideMenuItem.tsx @@ -13,6 +13,20 @@ type Props = { isDropdownList?: boolean } +const getActiveView = (path: string) => new URL(path, location.href).pathname.split('/')[1] + +const itemMatchesActiveView = (menuItem: MenuAsideItem, activeView: string): boolean => { + if (menuItem.href && getActiveView(menuItem.href) === activeView) { + return true + } + + if (!menuItem.menu?.length) { + return false + } + + return menuItem.menu.some((nestedItem) => itemMatchesActiveView(nestedItem, activeView)) +} + const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { const [isLinkActive, setIsLinkActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false) @@ -29,16 +43,20 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { const { asPath, isReady } = useRouter() useEffect(() => { - if (item.href && isReady) { - const linkPathName = new URL(item.href, location.href).pathname + '/'; - const activePathname = new URL(asPath, location.href).pathname - - const activeView = activePathname.split('/')[1]; - const linkPathNameView = linkPathName.split('/')[1]; - - setIsLinkActive(linkPathNameView === activeView); + if (!isReady) { + return } - }, [item.href, isReady, asPath]) + + const activeView = getActiveView(asPath) + + if (item.href) { + setIsLinkActive(getActiveView(item.href) === activeView) + } + + if (item.menu?.length) { + setIsDropdownActive(item.menu.some((nestedItem) => itemMatchesActiveView(nestedItem, activeView))) + } + }, [item.href, item.menu, isReady, asPath]) const asideMenuItemInnerContents = ( <> diff --git a/frontend/src/components/AsideMenuList.tsx b/frontend/src/components/AsideMenuList.tsx index 9ef6d84..6aa5420 100644 --- a/frontend/src/components/AsideMenuList.tsx +++ b/frontend/src/components/AsideMenuList.tsx @@ -3,7 +3,7 @@ import { MenuAsideItem } from '../interfaces' import AsideMenuItem from './AsideMenuItem' import { useAppSelector } from '../stores/hooks'; import { hasPermission } from '../helpers/userPermissions'; -import { getWorkspaceConfig, itemVisibleForRole } from '../helpers/workspace'; +import { getWorkspaceConfig, getWorkspaceRoute, itemVisibleForRole } from '../helpers/workspace'; type Props = { menu: MenuAsideItem[] @@ -32,6 +32,7 @@ export default function AsideMenuList({ menu, isDropdownList = false, className if (displayItem.label === 'Role Workspace') { displayItem.label = workspaceConfig.sidebarLabel; + displayItem.href = getWorkspaceRoute(roleName); } return ( diff --git a/frontend/src/helpers/workspace.ts b/frontend/src/helpers/workspace.ts index 2bde41c..e6c584d 100644 --- a/frontend/src/helpers/workspace.ts +++ b/frontend/src/helpers/workspace.ts @@ -9,6 +9,18 @@ export const WORKSPACE_ROLES = { public: 'Public', } as const; +export const WORKSPACE_ROUTES = { + [WORKSPACE_ROLES.superAdmin]: '/platform-command', + [WORKSPACE_ROLES.administrator]: '/operations-command', + [WORKSPACE_ROLES.directorGeneral]: '/executive-command', + [WORKSPACE_ROLES.financeDirector]: '/financial-control', + [WORKSPACE_ROLES.procurementLead]: '/procurement-desk', + [WORKSPACE_ROLES.complianceAuditLead]: '/compliance-desk', + [WORKSPACE_ROLES.projectDeliveryLead]: '/delivery-command', +} as const; + +const ADMIN_DASHBOARD_ROLES = [WORKSPACE_ROLES.superAdmin, WORKSPACE_ROLES.administrator] as const; + export type WorkspaceMetricKey = | 'approvedBudget' | 'committedBudget' @@ -197,114 +209,119 @@ const createWorkspaceConfig = ({ const workspaceConfigs: Record = { [WORKSPACE_ROLES.superAdmin]: createWorkspaceConfig({ sidebarLabel: 'Platform Command', - pageTitle: 'Super Administrator Workspace', - eyebrow: 'FDSU ERP · Super Administrator workspace', - heroTitle: 'Oversee tenants, access control, institutional activity, and platform-level risk from one command surface.', + pageTitle: 'Platform Command Workspace', + eyebrow: 'FDSU ERP · Tenant governance workspace', + heroTitle: 'Govern tenants, access policy, institutional activity, and platform-level risk from one command surface.', heroDescription: - 'This workspace prioritizes cross-organization visibility, access governance, auditability, and system stewardship while keeping drill-down access into the same ERP records used by operating teams.', - primaryAction: { href: '/organizations/organizations-list', label: 'Review organizations' }, - secondaryAction: { href: '/users/users-list', label: 'Manage users' }, + 'This workspace is for platform stewardship: organization oversight, role and permission control, auditability, and cross-tenant signals while preserving drill-down access into the underlying ERP records used by operating teams.', + primaryAction: { href: '/organizations/organizations-list', label: 'Review tenant registry' }, + secondaryAction: { href: '/users/users-list', label: 'Open access control' }, highlightedMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'contractsNearingExpiry', 'unreadNotifications', 'vendorComplianceAlerts'], heroMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'unreadNotifications'], blockOrder: ['focus', 'watchlist', 'summary', 'approvalRisk', 'operations', 'delivery', 'actions'], sectionCopy: { approvalQueue: { eyebrow: 'Governance queue', - title: 'Approvals and escalations needing platform attention', + title: 'Escalations and approvals requiring governance attention', actionLabel: 'Open governance queue', }, riskPanel: { eyebrow: 'Cross-cutting control flags', - title: 'Platform-wide exceptions and institutional exposure', + title: 'Platform-wide exceptions, permission exposure, and institutional control risk', }, recentNotifications: { - eyebrow: 'Platform activity', - title: 'Recent operational signals across tenants', + eyebrow: 'Cross-tenant activity', + title: 'Recent workflow and control signals across organizations', actionLabel: 'Open activity feed', }, quickActions: { eyebrow: 'Platform actions', - title: 'Jump directly into oversight and access-control work', + title: 'Jump directly into tenant oversight and permission-control work', }, }, quickLinks: [ { href: '/organizations/organizations-list', label: 'Organization registry', - description: 'Review tenant setup and coverage.', + description: 'Review tenant setup, coverage, and stewardship ownership.', icon: 'organizations', }, { href: '/users/users-list', label: 'User access', - description: 'Manage accounts, roles, and ownership.', + description: 'Manage accounts, authority assignments, and platform ownership.', icon: 'users', }, { - href: '/approvals/approvals-list', - label: 'Escalation queue', - description: 'Follow stalled approvals and control breaks.', - icon: 'approvals', + href: '/roles/roles-list', + label: 'Role catalog', + description: 'Govern platform-wide role definitions and responsibility boundaries.', + icon: 'users', }, { - href: '/audit_logs/audit_logs-list', - label: 'Audit trail', - description: 'Trace administrative and ERP record changes.', + href: '/permissions/permissions-list', + label: 'Permission matrix', + description: 'Inspect and tighten permission coverage across the platform.', icon: 'audit', }, ], }), [WORKSPACE_ROLES.administrator]: createWorkspaceConfig({ sidebarLabel: 'Operations Command', - pageTitle: 'Administrator Workspace', - eyebrow: 'FDSU ERP · Administrator workspace', - heroTitle: 'Run tenant operations, workflow readiness, user support, and master data hygiene without losing ERP traceability.', + pageTitle: 'Operations Command Workspace', + eyebrow: 'FDSU ERP · Workflow operations workspace', + heroTitle: 'Run workflow operations, user support, record readiness, and day-to-day administrative follow-through.', heroDescription: - 'This workspace is tuned for organization-level administration: approval setup, departments, notifications, operational bottlenecks, and administrative follow-through across the shared ERP.', - primaryAction: { href: '/approval_workflows/approval_workflows-list', label: 'Review workflows' }, - secondaryAction: { href: '/notifications/notifications-list', label: 'Check notifications' }, + 'This workspace is for organization-level operations administration: grouped workflow controls, notices, master-data upkeep, and fast drill-down into procurement, delivery, finance, and records without forcing teams to scan a single dense sidebar.', + primaryAction: { href: '/approval_workflows/approval_workflows-list', label: 'Open workflow hub' }, + secondaryAction: { href: '/notifications/notifications-list', label: 'Open notice center' }, highlightedMetricKeys: ['pendingApprovals', 'unreadNotifications', 'contractsNearingExpiry', 'openRiskAlerts', 'activeProjects', 'procurementPipeline'], heroMetricKeys: ['pendingApprovals', 'unreadNotifications', 'procurementPipeline', 'contractsNearingExpiry'], blockOrder: ['focus', 'summary', 'approvalRisk', 'watchlist', 'operations', 'delivery', 'actions'], sectionCopy: { approvalQueue: { eyebrow: 'Operations queue', - title: 'Approvals waiting on workflow administration', + title: 'Approvals waiting on routing, delegation, or admin follow-through', actionLabel: 'Open workflow queue', }, procurementQueue: { eyebrow: 'Operational throughput', - title: 'Requisitions that may require admin unblock', + title: 'Requisitions that may require workflow or setup unblock', actionLabel: 'Open requisitions', }, + recentNotifications: { + eyebrow: 'Operational notices', + title: 'Recent admin-facing signals requiring follow-through', + actionLabel: 'Open notice center', + }, quickActions: { eyebrow: 'Administrative actions', - title: 'Go straight to configuration and operational follow-through', + title: 'Open grouped control areas without scanning the full ERP register', }, }, quickLinks: [ { href: '/approval_workflows/approval_workflows-list', - label: 'Workflow setup', - description: 'Review approval design and routing.', + label: 'Workflow hub', + description: 'Keep approval routes, steps, and queues ready for daily use.', icon: 'approvals', }, { href: '/notifications/notifications-list', - label: 'Notification center', - description: 'Resolve unread system and user signals.', + label: 'Notice center', + description: 'Triage notifications, documents, and control signals in one stop.', icon: 'notifications', }, { href: '/users/users-list', - label: 'User support', - description: 'Update access and account records.', + label: 'Admin records', + description: 'Jump into users, departments, provinces, and support clean-up.', icon: 'users', }, { href: '/projects/projects-list', - label: 'Project register', - description: 'Verify record coverage and ownership.', + label: 'Delivery register', + description: 'Move from grouped navigation into active projects and contract follow-through.', icon: 'projects', }, ], @@ -616,6 +633,54 @@ export function getWorkspaceConfig(roleName?: string | null): WorkspaceConfig { return workspaceConfigs[roleName] || workspaceConfigs[WORKSPACE_ROLES.projectDeliveryLead]; } +export function getWorkspaceRoute(roleName?: string | null) { + if (!roleName) { + return '/executive-summary'; + } + + return WORKSPACE_ROUTES[roleName] || '/executive-summary'; +} + +export function normalizeInternalRedirectTarget(target?: string | string[] | null) { + const redirectTarget = Array.isArray(target) ? target[0] : target; + + if (!redirectTarget || typeof redirectTarget !== 'string') { + return null; + } + + if (!redirectTarget.startsWith('/') || redirectTarget.startsWith('//')) { + return null; + } + + if (redirectTarget.startsWith('/login')) { + return null; + } + + return redirectTarget; +} + +export function getPostLoginRoute(roleName?: string | null, redirectTarget?: string | string[] | null) { + return normalizeInternalRedirectTarget(redirectTarget) || getWorkspaceRoute(roleName); +} + +export function getLoginRoute(redirectTarget?: string | string[] | null) { + const safeRedirectTarget = normalizeInternalRedirectTarget(redirectTarget); + + if (!safeRedirectTarget) { + return '/login'; + } + + return `/login?redirect=${encodeURIComponent(safeRedirectTarget)}`; +} + +export function isDashboardRole(roleName?: string | null) { + if (!roleName) { + return false; + } + + return ADMIN_DASHBOARD_ROLES.includes(roleName as (typeof ADMIN_DASHBOARD_ROLES)[number]); +} + export function itemVisibleForRole(itemRoles?: string[], roleName?: string | null) { if (!itemRoles?.length) { return true; diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 73d8391..3b5fb16 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -14,6 +14,7 @@ import { useRouter } from 'next/router' import {findMe, logoutUser} from "../stores/authSlice"; import {hasPermission} from "../helpers/userPermissions"; +import { getLoginRoute } from "../helpers/workspace"; type Props = { @@ -49,12 +50,14 @@ export default function LayoutAuthenticated({ }; useEffect(() => { - dispatch(findMe()); if (!isTokenValid()) { dispatch(logoutUser()); - router.push('/login'); + router.replace(getLoginRoute(router.asPath)); + return; } - }, [token, localToken]); + + dispatch(findMe()); + }, [dispatch, localToken, router, token]); useEffect(() => { diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 6b5a8e3..2d470b6 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -1,7 +1,705 @@ -import * as icon from '@mdi/js'; +import * as icon from '@mdi/js' import { MenuAsideItem } from './interfaces' import { WORKSPACE_ROLES } from './helpers/workspace' +const optionalIcon = (name: string, fallback: string = icon.mdiTable): string => { + const iconSet = icon as Record + + return iconSet[name] || fallback +} + +const adminWorkspaceRoles = [WORKSPACE_ROLES.administrator] +const sharedEntityRoles = [ + WORKSPACE_ROLES.superAdmin, + WORKSPACE_ROLES.directorGeneral, + WORKSPACE_ROLES.financeDirector, + WORKSPACE_ROLES.procurementLead, + WORKSPACE_ROLES.complianceAuditLead, + WORKSPACE_ROLES.projectDeliveryLead, +] + +const adminGroupedNavigation: MenuAsideItem[] = [ + { + label: 'Workflow Readiness', + icon: icon.mdiSitemap, + roles: adminWorkspaceRoles, + withDevider: true, + menu: [ + { + href: '/approval_workflows/approval_workflows-list', + label: 'Approval workflows', + icon: optionalIcon('mdiSitemap'), + permissions: 'READ_APPROVAL_WORKFLOWS', + }, + { + href: '/approval_steps/approval_steps-list', + label: 'Approval steps', + icon: optionalIcon('mdiStairs'), + permissions: 'READ_APPROVAL_STEPS', + }, + { + href: '/approvals/approvals-list', + label: 'Approvals', + icon: optionalIcon('mdiChecklist'), + permissions: 'READ_APPROVALS', + }, + { + href: '/role_permissions/role_permissions-list', + label: 'Role access', + icon: optionalIcon('mdiLinkVariant'), + permissions: 'READ_ROLE_PERMISSIONS', + }, + ], + }, + { + label: 'Operational Notices', + icon: icon.mdiBellOutline, + roles: adminWorkspaceRoles, + menu: [ + { + href: '/notifications/notifications-list', + label: 'Notifications', + icon: optionalIcon('mdiBell'), + permissions: 'READ_NOTIFICATIONS', + }, + { + href: '/audit_logs/audit_logs-list', + label: 'Audit logs', + icon: optionalIcon('mdiClipboardTextClock'), + permissions: 'READ_AUDIT_LOGS', + }, + { + href: '/documents/documents-list', + label: 'Documents', + icon: optionalIcon('mdiFolderFile'), + permissions: 'READ_DOCUMENTS', + }, + { + href: '/compliance_alerts/compliance_alerts-list', + label: 'Compliance alerts', + icon: optionalIcon('mdiShieldAlert'), + permissions: 'READ_COMPLIANCE_ALERTS', + }, + ], + }, + { + label: 'Administration', + icon: icon.mdiAccountGroup, + roles: adminWorkspaceRoles, + menu: [ + { + href: '/users/users-list', + label: 'Users', + icon: icon.mdiAccountGroup, + permissions: 'READ_USERS', + }, + { + href: '/provinces/provinces-list', + label: 'Provinces', + icon: optionalIcon('mdiMapMarker'), + permissions: 'READ_PROVINCES', + }, + { + href: '/departments/departments-list', + label: 'Departments', + icon: optionalIcon('mdiOfficeBuilding'), + permissions: 'READ_DEPARTMENTS', + }, + ], + }, + { + label: 'Budget & Planning', + icon: optionalIcon('mdiWalletOutline'), + roles: adminWorkspaceRoles, + menu: [ + { + href: '/fiscal_years/fiscal_years-list', + label: 'Fiscal years', + icon: optionalIcon('mdiCalendarRange'), + permissions: 'READ_FISCAL_YEARS', + }, + { + href: '/funding_sources/funding_sources-list', + label: 'Funding sources', + icon: optionalIcon('mdiCashMultiple'), + permissions: 'READ_FUNDING_SOURCES', + }, + { + href: '/budget_programs/budget_programs-list', + label: 'Budget programs', + icon: optionalIcon('mdiChartDonut'), + permissions: 'READ_BUDGET_PROGRAMS', + }, + { + href: '/budget_lines/budget_lines-list', + label: 'Budget lines', + icon: optionalIcon('mdiFormatListBulleted'), + permissions: 'READ_BUDGET_LINES', + }, + { + href: '/allocations/allocations-list', + label: 'Allocations', + icon: optionalIcon('mdiDatabaseArrowRight'), + permissions: 'READ_ALLOCATIONS', + }, + { + href: '/budget_reallocations/budget_reallocations-list', + label: 'Reallocations', + icon: optionalIcon('mdiSwapHorizontal'), + permissions: 'READ_BUDGET_REALLOCATIONS', + }, + ], + }, + { + label: 'Procurement', + icon: icon.mdiGavel, + roles: adminWorkspaceRoles, + menu: [ + { + href: '/procurement_plans/procurement_plans-list', + label: 'Plans', + icon: optionalIcon('mdiClipboardList'), + permissions: 'READ_PROCUREMENT_PLANS', + }, + { + href: '/requisitions/requisitions-list', + label: 'Requisitions', + icon: optionalIcon('mdiFileDocumentEdit'), + permissions: 'READ_REQUISITIONS', + }, + { + href: '/tenders/tenders-list', + label: 'Tenders', + icon: optionalIcon('mdiGavel'), + permissions: 'READ_TENDERS', + }, + { + href: '/vendors/vendors-list', + label: 'Vendors', + icon: optionalIcon('mdiTruckFast'), + permissions: 'READ_VENDORS', + }, + { + href: '/vendor_compliance_documents/vendor_compliance_documents-list', + label: 'Vendor compliance', + icon: optionalIcon('mdiFileCertificate'), + permissions: 'READ_VENDOR_COMPLIANCE_DOCUMENTS', + }, + { + href: '/bids/bids-list', + label: 'Bids', + icon: optionalIcon('mdiFileSign'), + permissions: 'READ_BIDS', + }, + { + href: '/bid_evaluations/bid_evaluations-list', + label: 'Bid evaluations', + icon: optionalIcon('mdiClipboardCheck'), + permissions: 'READ_BID_EVALUATIONS', + }, + { + href: '/awards/awards-list', + label: 'Awards', + icon: optionalIcon('mdiTrophyAward'), + permissions: 'READ_AWARDS', + }, + ], + }, + { + label: 'Delivery & Contracts', + icon: icon.mdiChartTimelineVariant, + roles: adminWorkspaceRoles, + menu: [ + { + href: '/programs/programs-list', + label: 'Programs', + icon: optionalIcon('mdiViewGridOutline'), + permissions: 'READ_PROGRAMS', + }, + { + href: '/projects/projects-list', + label: 'Projects', + icon: optionalIcon('mdiBriefcaseCheck'), + permissions: 'READ_PROJECTS', + }, + { + href: '/project_milestones/project_milestones-list', + label: 'Milestones', + icon: optionalIcon('mdiTimelineCheck'), + permissions: 'READ_PROJECT_MILESTONES', + }, + { + href: '/risks/risks-list', + label: 'Risks', + icon: optionalIcon('mdiAlertOctagon'), + permissions: 'READ_RISKS', + }, + { + href: '/issues/issues-list', + label: 'Issues', + icon: optionalIcon('mdiBug'), + permissions: 'READ_ISSUES', + }, + { + href: '/field_verifications/field_verifications-list', + label: 'Field checks', + icon: optionalIcon('mdiMapMarkerCheck'), + permissions: 'READ_FIELD_VERIFICATIONS', + }, + { + href: '/contracts/contracts-list', + label: 'Contracts', + icon: optionalIcon('mdiFileDocumentOutline'), + permissions: 'READ_CONTRACTS', + }, + { + href: '/contract_amendments/contract_amendments-list', + label: 'Amendments', + icon: optionalIcon('mdiFileReplaceOutline'), + permissions: 'READ_CONTRACT_AMENDMENTS', + }, + { + href: '/contract_milestones/contract_milestones-list', + label: 'Contract milestones', + icon: optionalIcon('mdiTimelineCheck'), + permissions: 'READ_CONTRACT_MILESTONES', + }, + ], + }, + { + label: 'Grants & Beneficiaries', + icon: optionalIcon('mdiHandCoin'), + roles: adminWorkspaceRoles, + menu: [ + { + href: '/grants/grants-list', + label: 'Grants', + icon: optionalIcon('mdiHandCoin'), + permissions: 'READ_GRANTS', + }, + { + href: '/beneficiaries/beneficiaries-list', + label: 'Beneficiaries', + icon: optionalIcon('mdiAccountGroup'), + permissions: 'READ_BENEFICIARIES', + }, + { + href: '/grant_applications/grant_applications-list', + label: 'Applications', + icon: optionalIcon('mdiFileDocumentMultiple'), + permissions: 'READ_GRANT_APPLICATIONS', + }, + { + href: '/grant_evaluations/grant_evaluations-list', + label: 'Evaluations', + icon: optionalIcon('mdiStarCheck'), + permissions: 'READ_GRANT_EVALUATIONS', + }, + { + href: '/grant_tranches/grant_tranches-list', + label: 'Tranches', + icon: optionalIcon('mdiCashCheck'), + permissions: 'READ_GRANT_TRANCHES', + }, + ], + }, + { + label: 'Finance & Payments', + icon: optionalIcon('mdiCashCheck'), + roles: adminWorkspaceRoles, + menu: [ + { + href: '/expense_categories/expense_categories-list', + label: 'Expense categories', + icon: optionalIcon('mdiTagMultiple'), + permissions: 'READ_EXPENSE_CATEGORIES', + }, + { + href: '/invoices/invoices-list', + label: 'Invoices', + icon: optionalIcon('mdiReceiptText'), + permissions: 'READ_INVOICES', + }, + { + href: '/payment_requests/payment_requests-list', + label: 'Payment requests', + icon: optionalIcon('mdiCashFast'), + permissions: 'READ_PAYMENT_REQUESTS', + }, + { + href: '/payment_batches/payment_batches-list', + label: 'Payment batches', + icon: optionalIcon('mdiPackageVariantClosed'), + permissions: 'READ_PAYMENT_BATCHES', + }, + { + href: '/payments/payments-list', + label: 'Payments', + icon: optionalIcon('mdiBankTransfer'), + permissions: 'READ_PAYMENTS', + }, + { + href: '/obligations/obligations-list', + label: 'Obligations', + icon: optionalIcon('mdiBookArrowDown'), + permissions: 'READ_OBLIGATIONS', + }, + { + href: '/ledger_entries/ledger_entries-list', + label: 'Ledger entries', + icon: optionalIcon('mdiBookOpenPageVariant'), + permissions: 'READ_LEDGER_ENTRIES', + }, + ], + }, +] + +const sharedEntityNavigation: MenuAsideItem[] = [ + { + href: '/users/users-list', + label: 'Users', + roles: [WORKSPACE_ROLES.superAdmin], + icon: icon.mdiAccountGroup, + permissions: 'READ_USERS', + }, + { + href: '/roles/roles-list', + label: 'Roles', + roles: [WORKSPACE_ROLES.superAdmin], + icon: optionalIcon('mdiShieldAccountVariantOutline'), + permissions: 'READ_ROLES', + }, + { + href: '/permissions/permissions-list', + label: 'Permissions', + roles: [WORKSPACE_ROLES.superAdmin], + icon: optionalIcon('mdiShieldAccountOutline'), + permissions: 'READ_PERMISSIONS', + }, + { + href: '/organizations/organizations-list', + label: 'Organizations', + roles: [WORKSPACE_ROLES.superAdmin], + icon: optionalIcon('mdiDomain'), + permissions: 'READ_ORGANIZATIONS', + }, + { + href: '/provinces/provinces-list', + label: 'Provinces', + roles: sharedEntityRoles, + icon: optionalIcon('mdiMapMarker'), + permissions: 'READ_PROVINCES', + }, + { + href: '/departments/departments-list', + label: 'Departments', + roles: sharedEntityRoles, + icon: optionalIcon('mdiOfficeBuilding'), + permissions: 'READ_DEPARTMENTS', + }, + { + href: '/role_permissions/role_permissions-list', + label: 'Role permissions', + roles: sharedEntityRoles, + icon: optionalIcon('mdiLinkVariant'), + permissions: 'READ_ROLE_PERMISSIONS', + }, + { + href: '/approval_workflows/approval_workflows-list', + label: 'Approval workflows', + roles: sharedEntityRoles, + icon: optionalIcon('mdiSitemap'), + permissions: 'READ_APPROVAL_WORKFLOWS', + }, + { + href: '/approval_steps/approval_steps-list', + label: 'Approval steps', + roles: sharedEntityRoles, + icon: optionalIcon('mdiStairs'), + permissions: 'READ_APPROVAL_STEPS', + }, + { + href: '/approvals/approvals-list', + label: 'Approvals', + roles: sharedEntityRoles, + icon: optionalIcon('mdiChecklist'), + permissions: 'READ_APPROVALS', + }, + { + href: '/notifications/notifications-list', + label: 'Notifications', + roles: sharedEntityRoles, + icon: optionalIcon('mdiBell'), + permissions: 'READ_NOTIFICATIONS', + }, + { + href: '/audit_logs/audit_logs-list', + label: 'Audit logs', + roles: sharedEntityRoles, + icon: optionalIcon('mdiClipboardTextClock'), + permissions: 'READ_AUDIT_LOGS', + }, + { + href: '/fiscal_years/fiscal_years-list', + label: 'Fiscal years', + roles: sharedEntityRoles, + icon: optionalIcon('mdiCalendarRange'), + permissions: 'READ_FISCAL_YEARS', + }, + { + href: '/funding_sources/funding_sources-list', + label: 'Funding sources', + roles: sharedEntityRoles, + icon: optionalIcon('mdiCashMultiple'), + permissions: 'READ_FUNDING_SOURCES', + }, + { + href: '/budget_programs/budget_programs-list', + label: 'Budget programs', + roles: sharedEntityRoles, + icon: optionalIcon('mdiChartDonut'), + permissions: 'READ_BUDGET_PROGRAMS', + }, + { + href: '/budget_lines/budget_lines-list', + label: 'Budget lines', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFormatListBulleted'), + permissions: 'READ_BUDGET_LINES', + }, + { + href: '/allocations/allocations-list', + label: 'Allocations', + roles: sharedEntityRoles, + icon: optionalIcon('mdiDatabaseArrowRight'), + permissions: 'READ_ALLOCATIONS', + }, + { + href: '/budget_reallocations/budget_reallocations-list', + label: 'Budget reallocations', + roles: sharedEntityRoles, + icon: optionalIcon('mdiSwapHorizontal'), + permissions: 'READ_BUDGET_REALLOCATIONS', + }, + { + href: '/procurement_plans/procurement_plans-list', + label: 'Procurement plans', + roles: sharedEntityRoles, + icon: optionalIcon('mdiClipboardList'), + permissions: 'READ_PROCUREMENT_PLANS', + }, + { + href: '/requisitions/requisitions-list', + label: 'Requisitions', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFileDocumentEdit'), + permissions: 'READ_REQUISITIONS', + }, + { + href: '/tenders/tenders-list', + label: 'Tenders', + roles: sharedEntityRoles, + icon: optionalIcon('mdiGavel'), + permissions: 'READ_TENDERS', + }, + { + href: '/vendors/vendors-list', + label: 'Vendors', + roles: sharedEntityRoles, + icon: optionalIcon('mdiTruckFast'), + permissions: 'READ_VENDORS', + }, + { + href: '/vendor_compliance_documents/vendor_compliance_documents-list', + label: 'Vendor compliance documents', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFileCertificate'), + permissions: 'READ_VENDOR_COMPLIANCE_DOCUMENTS', + }, + { + href: '/bids/bids-list', + label: 'Bids', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFileSign'), + permissions: 'READ_BIDS', + }, + { + href: '/bid_evaluations/bid_evaluations-list', + label: 'Bid evaluations', + roles: sharedEntityRoles, + icon: optionalIcon('mdiClipboardCheck'), + permissions: 'READ_BID_EVALUATIONS', + }, + { + href: '/awards/awards-list', + label: 'Awards', + roles: sharedEntityRoles, + icon: optionalIcon('mdiTrophyAward'), + permissions: 'READ_AWARDS', + }, + { + href: '/programs/programs-list', + label: 'Programs', + roles: sharedEntityRoles, + icon: optionalIcon('mdiViewGridOutline'), + permissions: 'READ_PROGRAMS', + }, + { + href: '/projects/projects-list', + label: 'Projects', + roles: sharedEntityRoles, + icon: optionalIcon('mdiBriefcaseCheck'), + permissions: 'READ_PROJECTS', + }, + { + href: '/project_milestones/project_milestones-list', + label: 'Project milestones', + roles: sharedEntityRoles, + icon: optionalIcon('mdiTimelineCheck'), + permissions: 'READ_PROJECT_MILESTONES', + }, + { + href: '/risks/risks-list', + label: 'Risks', + roles: sharedEntityRoles, + icon: optionalIcon('mdiAlertOctagon'), + permissions: 'READ_RISKS', + }, + { + href: '/issues/issues-list', + label: 'Issues', + roles: sharedEntityRoles, + icon: optionalIcon('mdiBug'), + permissions: 'READ_ISSUES', + }, + { + href: '/field_verifications/field_verifications-list', + label: 'Field verifications', + roles: sharedEntityRoles, + icon: optionalIcon('mdiMapMarkerCheck'), + permissions: 'READ_FIELD_VERIFICATIONS', + }, + { + href: '/contracts/contracts-list', + label: 'Contracts', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFileDocumentOutline'), + permissions: 'READ_CONTRACTS', + }, + { + href: '/contract_amendments/contract_amendments-list', + label: 'Contract amendments', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFileReplaceOutline'), + permissions: 'READ_CONTRACT_AMENDMENTS', + }, + { + href: '/contract_milestones/contract_milestones-list', + label: 'Contract milestones', + roles: sharedEntityRoles, + icon: optionalIcon('mdiTimelineCheck'), + permissions: 'READ_CONTRACT_MILESTONES', + }, + { + href: '/grants/grants-list', + label: 'Grants', + roles: sharedEntityRoles, + icon: optionalIcon('mdiHandCoin'), + permissions: 'READ_GRANTS', + }, + { + href: '/beneficiaries/beneficiaries-list', + label: 'Beneficiaries', + roles: sharedEntityRoles, + icon: optionalIcon('mdiAccountGroup'), + permissions: 'READ_BENEFICIARIES', + }, + { + href: '/grant_applications/grant_applications-list', + label: 'Grant applications', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFileDocumentMultiple'), + permissions: 'READ_GRANT_APPLICATIONS', + }, + { + href: '/grant_evaluations/grant_evaluations-list', + label: 'Grant evaluations', + roles: sharedEntityRoles, + icon: optionalIcon('mdiStarCheck'), + permissions: 'READ_GRANT_EVALUATIONS', + }, + { + href: '/grant_tranches/grant_tranches-list', + label: 'Grant tranches', + roles: sharedEntityRoles, + icon: optionalIcon('mdiCashCheck'), + permissions: 'READ_GRANT_TRANCHES', + }, + { + href: '/expense_categories/expense_categories-list', + label: 'Expense categories', + roles: sharedEntityRoles, + icon: optionalIcon('mdiTagMultiple'), + permissions: 'READ_EXPENSE_CATEGORIES', + }, + { + href: '/invoices/invoices-list', + label: 'Invoices', + roles: sharedEntityRoles, + icon: optionalIcon('mdiReceiptText'), + permissions: 'READ_INVOICES', + }, + { + href: '/payment_requests/payment_requests-list', + label: 'Payment requests', + roles: sharedEntityRoles, + icon: optionalIcon('mdiCashFast'), + permissions: 'READ_PAYMENT_REQUESTS', + }, + { + href: '/payment_batches/payment_batches-list', + label: 'Payment batches', + roles: sharedEntityRoles, + icon: optionalIcon('mdiPackageVariantClosed'), + permissions: 'READ_PAYMENT_BATCHES', + }, + { + href: '/payments/payments-list', + label: 'Payments', + roles: sharedEntityRoles, + icon: optionalIcon('mdiBankTransfer'), + permissions: 'READ_PAYMENTS', + }, + { + href: '/obligations/obligations-list', + label: 'Obligations', + roles: sharedEntityRoles, + icon: optionalIcon('mdiBookArrowDown'), + permissions: 'READ_OBLIGATIONS', + }, + { + href: '/ledger_entries/ledger_entries-list', + label: 'Ledger entries', + roles: sharedEntityRoles, + icon: optionalIcon('mdiBookOpenPageVariant'), + permissions: 'READ_LEDGER_ENTRIES', + }, + { + href: '/documents/documents-list', + label: 'Documents', + roles: sharedEntityRoles, + icon: optionalIcon('mdiFolderFile'), + permissions: 'READ_DOCUMENTS', + }, + { + href: '/compliance_alerts/compliance_alerts-list', + label: 'Compliance alerts', + roles: sharedEntityRoles, + icon: optionalIcon('mdiShieldAlert'), + permissions: 'READ_COMPLIANCE_ALERTS', + }, +] + const menuAside: MenuAsideItem[] = [ { href: '/executive-summary', @@ -20,7 +718,11 @@ const menuAside: MenuAsideItem[] = [ { href: '/dashboard', icon: icon.mdiViewDashboardOutline, - label: 'Platform Widgets', + label: 'Admin Widgets', + labelByRole: { + [WORKSPACE_ROLES.superAdmin]: 'Platform Widgets', + [WORKSPACE_ROLES.administrator]: 'Operations Widgets', + }, roles: [WORKSPACE_ROLES.superAdmin, WORKSPACE_ROLES.administrator], }, { @@ -38,21 +740,7 @@ const menuAside: MenuAsideItem[] = [ permissions: 'READ_USERS', roles: [WORKSPACE_ROLES.superAdmin], }, - { - href: '/approval_workflows/approval_workflows-list', - label: 'Workflow Readiness', - icon: icon.mdiSitemap, - permissions: 'READ_APPROVAL_WORKFLOWS', - roles: [WORKSPACE_ROLES.administrator], - withDevider: true, - }, - { - href: '/notifications/notifications-list', - label: 'Operational Notices', - icon: icon.mdiBellOutline, - permissions: 'READ_NOTIFICATIONS', - roles: [WORKSPACE_ROLES.administrator], - }, + ...adminGroupedNavigation, { href: '/projects/projects-list', label: 'Strategic Portfolio', @@ -128,412 +816,18 @@ const menuAside: MenuAsideItem[] = [ permissions: 'READ_PROJECT_MILESTONES', roles: [WORKSPACE_ROLES.projectDeliveryLead], }, - - { - href: '/users/users-list', - label: 'Users', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiAccountGroup ?? icon.mdiTable, - permissions: 'READ_USERS' - }, - { - href: '/roles/roles-list', - label: 'Roles', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, - permissions: 'READ_ROLES' - }, - { - href: '/permissions/permissions-list', - label: 'Permissions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, - permissions: 'READ_PERMISSIONS' - }, - { - href: '/organizations/organizations-list', - label: 'Organizations', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_ORGANIZATIONS' - }, - { - href: '/provinces/provinces-list', - label: 'Provinces', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PROVINCES' - }, - { - href: '/departments/departments-list', - label: 'Departments', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiOfficeBuilding' in icon ? icon['mdiOfficeBuilding' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_DEPARTMENTS' - }, - { - href: '/role_permissions/role_permissions-list', - label: 'Role permissions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_ROLE_PERMISSIONS' - }, - { - href: '/approval_workflows/approval_workflows-list', - label: 'Approval workflows', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiSitemap' in icon ? icon['mdiSitemap' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_APPROVAL_WORKFLOWS' - }, - { - href: '/approval_steps/approval_steps-list', - label: 'Approval steps', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiStairs' in icon ? icon['mdiStairs' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_APPROVAL_STEPS' - }, - { - href: '/approvals/approvals-list', - label: 'Approvals', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiChecklist' in icon ? icon['mdiChecklist' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_APPROVALS' - }, - { - href: '/notifications/notifications-list', - label: 'Notifications', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiBell' in icon ? icon['mdiBell' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_NOTIFICATIONS' - }, - { - href: '/audit_logs/audit_logs-list', - label: 'Audit logs', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiClipboardTextClock' in icon ? icon['mdiClipboardTextClock' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_AUDIT_LOGS' - }, - { - href: '/fiscal_years/fiscal_years-list', - label: 'Fiscal years', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiCalendarRange' in icon ? icon['mdiCalendarRange' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_FISCAL_YEARS' - }, - { - href: '/funding_sources/funding_sources-list', - label: 'Funding sources', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiCashMultiple' in icon ? icon['mdiCashMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_FUNDING_SOURCES' - }, - { - href: '/budget_programs/budget_programs-list', - label: 'Budget programs', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiChartDonut' in icon ? icon['mdiChartDonut' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BUDGET_PROGRAMS' - }, - { - href: '/budget_lines/budget_lines-list', - label: 'Budget lines', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BUDGET_LINES' - }, - { - href: '/allocations/allocations-list', - label: 'Allocations', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiDatabaseArrowRight' in icon ? icon['mdiDatabaseArrowRight' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_ALLOCATIONS' - }, - { - href: '/budget_reallocations/budget_reallocations-list', - label: 'Budget reallocations', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BUDGET_REALLOCATIONS' - }, - { - href: '/procurement_plans/procurement_plans-list', - label: 'Procurement plans', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiClipboardList' in icon ? icon['mdiClipboardList' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PROCUREMENT_PLANS' - }, - { - href: '/requisitions/requisitions-list', - label: 'Requisitions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileDocumentEdit' in icon ? icon['mdiFileDocumentEdit' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_REQUISITIONS' - }, - { - href: '/tenders/tenders-list', - label: 'Tenders', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiGavel' in icon ? icon['mdiGavel' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_TENDERS' - }, - { - href: '/vendors/vendors-list', - label: 'Vendors', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiTruckFast' in icon ? icon['mdiTruckFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_VENDORS' - }, - { - href: '/vendor_compliance_documents/vendor_compliance_documents-list', - label: 'Vendor compliance documents', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileCertificate' in icon ? icon['mdiFileCertificate' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_VENDOR_COMPLIANCE_DOCUMENTS' - }, - { - href: '/bids/bids-list', - label: 'Bids', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileSign' in icon ? icon['mdiFileSign' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BIDS' - }, - { - href: '/bid_evaluations/bid_evaluations-list', - label: 'Bid evaluations', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiClipboardCheck' in icon ? icon['mdiClipboardCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BID_EVALUATIONS' - }, - { - href: '/awards/awards-list', - label: 'Awards', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiTrophyAward' in icon ? icon['mdiTrophyAward' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_AWARDS' - }, - { - href: '/programs/programs-list', - label: 'Programs', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiViewGridOutline' in icon ? icon['mdiViewGridOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PROGRAMS' - }, - { - href: '/projects/projects-list', - label: 'Projects', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiBriefcaseCheck' in icon ? icon['mdiBriefcaseCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PROJECTS' - }, - { - href: '/project_milestones/project_milestones-list', - label: 'Project milestones', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFlagCheckered' in icon ? icon['mdiFlagCheckered' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PROJECT_MILESTONES' - }, - { - href: '/risks/risks-list', - label: 'Risks', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiAlertOctagon' in icon ? icon['mdiAlertOctagon' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_RISKS' - }, - { - href: '/issues/issues-list', - label: 'Issues', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiBug' in icon ? icon['mdiBug' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_ISSUES' - }, - { - href: '/field_verifications/field_verifications-list', - label: 'Field verifications', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiMapMarkerCheck' in icon ? icon['mdiMapMarkerCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_FIELD_VERIFICATIONS' - }, - { - href: '/contracts/contracts-list', - label: 'Contracts', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileDocumentOutline' in icon ? icon['mdiFileDocumentOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_CONTRACTS' - }, - { - href: '/contract_amendments/contract_amendments-list', - label: 'Contract amendments', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileReplaceOutline' in icon ? icon['mdiFileReplaceOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_CONTRACT_AMENDMENTS' - }, - { - href: '/contract_milestones/contract_milestones-list', - label: 'Contract milestones', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiTimelineCheck' in icon ? icon['mdiTimelineCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_CONTRACT_MILESTONES' - }, - { - href: '/grants/grants-list', - label: 'Grants', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiHandCoin' in icon ? icon['mdiHandCoin' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_GRANTS' - }, - { - href: '/beneficiaries/beneficiaries-list', - label: 'Beneficiaries', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_BENEFICIARIES' - }, - { - href: '/grant_applications/grant_applications-list', - label: 'Grant applications', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileDocumentMultiple' in icon ? icon['mdiFileDocumentMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_GRANT_APPLICATIONS' - }, - { - href: '/grant_evaluations/grant_evaluations-list', - label: 'Grant evaluations', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiStarCheck' in icon ? icon['mdiStarCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_GRANT_EVALUATIONS' - }, - { - href: '/grant_tranches/grant_tranches-list', - label: 'Grant tranches', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiCashCheck' in icon ? icon['mdiCashCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_GRANT_TRANCHES' - }, - { - href: '/expense_categories/expense_categories-list', - label: 'Expense categories', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiTagMultiple' in icon ? icon['mdiTagMultiple' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_EXPENSE_CATEGORIES' - }, - { - href: '/invoices/invoices-list', - label: 'Invoices', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiReceiptText' in icon ? icon['mdiReceiptText' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_INVOICES' - }, - { - href: '/payment_requests/payment_requests-list', - label: 'Payment requests', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiCashFast' in icon ? icon['mdiCashFast' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PAYMENT_REQUESTS' - }, - { - href: '/payment_batches/payment_batches-list', - label: 'Payment batches', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiPackageVariantClosed' in icon ? icon['mdiPackageVariantClosed' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PAYMENT_BATCHES' - }, - { - href: '/payments/payments-list', - label: 'Payments', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiBankTransfer' in icon ? icon['mdiBankTransfer' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_PAYMENTS' - }, - { - href: '/obligations/obligations-list', - label: 'Obligations', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiBookArrowDown' in icon ? icon['mdiBookArrowDown' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_OBLIGATIONS' - }, - { - href: '/ledger_entries/ledger_entries-list', - label: 'Ledger entries', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiBookOpenPageVariant' in icon ? icon['mdiBookOpenPageVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_LEDGER_ENTRIES' - }, - { - href: '/documents/documents-list', - label: 'Documents', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFolderFile' in icon ? icon['mdiFolderFile' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_DOCUMENTS' - }, - { - href: '/compliance_alerts/compliance_alerts-list', - label: 'Compliance alerts', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiShieldAlert' in icon ? icon['mdiShieldAlert' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_COMPLIANCE_ALERTS' - }, + ...sharedEntityNavigation, { href: '/profile', label: 'Profile', icon: icon.mdiAccountCircle, }, - - { href: '/api-docs', target: '_blank', label: 'Swagger API', icon: icon.mdiFileCode, - permissions: 'READ_API_DOCS' + permissions: 'READ_API_DOCS', }, ] diff --git a/frontend/src/pages/compliance-desk.tsx b/frontend/src/pages/compliance-desk.tsx new file mode 100644 index 0000000..e767487 --- /dev/null +++ b/frontend/src/pages/compliance-desk.tsx @@ -0,0 +1,7 @@ +import ExecutiveSummaryPage from './executive-summary'; + +const ComplianceDeskPage: any = ExecutiveSummaryPage; + +ComplianceDeskPage.getLayout = (ExecutiveSummaryPage as any).getLayout; + +export default ComplianceDeskPage; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 1747e62..1edfeaa 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -1,16 +1,19 @@ import * as icon from '@mdi/js'; import Head from 'next/head' +import { useRouter } from 'next/router' import React from 'react' import axios from 'axios'; import type { ReactElement } from 'react' import LayoutAuthenticated from '../layouts/Authenticated' import SectionMain from '../components/SectionMain' +import CardBox from '../components/CardBox' import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' import BaseIcon from "../components/BaseIcon"; import { getPageTitle } from '../config' import Link from "next/link"; import { hasPermission } from "../helpers/userPermissions"; +import { getWorkspaceRoute, isDashboardRole } from '../helpers/workspace'; import { fetchWidgets } from '../stores/roles/rolesSlice'; import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { SmartWidget } from '../components/SmartWidget/SmartWidget'; @@ -18,6 +21,7 @@ import { SmartWidget } from '../components/SmartWidget/SmartWidget'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; const Dashboard = () => { const dispatch = useAppDispatch(); + const router = useRouter(); const iconsColor = useAppSelector((state) => state.style.iconsColor); const corners = useAppSelector((state) => state.style.corners); const cardsStyle = useAppSelector((state) => state.style.cardsStyle); @@ -83,7 +87,22 @@ const Dashboard = () => { const { isFetchingQuery } = useAppSelector((state) => state.openAi); const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - + const roleName = currentUser?.app_role?.name; + const dashboardAllowed = isDashboardRole(roleName); + const workspaceRoute = getWorkspaceRoute(roleName); + const dashboardCopy = roleName === 'Super Administrator' + ? { + pageTitle: 'Platform Widgets', + eyebrow: 'Super Administrator widget surface', + description: 'Configure and review widgets for tenant governance, access control, and cross-organization oversight.', + widgetLabelFallback: 'Super Administrator', + } + : { + pageTitle: 'Operations Widgets', + eyebrow: 'Administrator operations surface', + description: 'Configure and review widgets for workflow readiness, operational notices, and day-to-day administrative follow-through.', + widgetLabelFallback: 'Administrator', + }; const organizationId = currentUser?.organizations?.id; @@ -116,6 +135,12 @@ const Dashboard = () => { async function getWidgets(roleId) { await dispatch(fetchWidgets(roleId)); } + React.useEffect(() => { + if (!currentUser?.id || dashboardAllowed) return; + + router.replace(workspaceRoute); + }, [currentUser?.id, dashboardAllowed, router, workspaceRoute]); + React.useEffect(() => { if (!currentUser) return; loadData().then(); @@ -127,20 +152,42 @@ const Dashboard = () => { getWidgets(widgetsRole?.role?.value || '').then(); }, [widgetsRole?.role?.value]); + if (currentUser?.id && !dashboardAllowed) { + return ( + + +
+

Role workspace redirect

+

This dashboard is reserved for Super Administrators and Administrators.

+

Redirecting you to your role workspace now.

+
+ Go to workspace now +
+
+
+
+ ) + } + return ( <> - {getPageTitle('Overview')} + {getPageTitle(dashboardCopy.pageTitle)} {''} + + +

{dashboardCopy.eyebrow}

+

{dashboardCopy.description}

+
{hasPermission(currentUser, 'CREATE_ROLES') && { {!!rolesWidgets.length && hasPermission(currentUser, 'CREATE_ROLES') && (

- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} + {`${widgetsRole?.role?.label || dashboardCopy.widgetLabelFallback}'s widgets`}

)} diff --git a/frontend/src/pages/delivery-command.tsx b/frontend/src/pages/delivery-command.tsx new file mode 100644 index 0000000..4149abf --- /dev/null +++ b/frontend/src/pages/delivery-command.tsx @@ -0,0 +1,7 @@ +import ExecutiveSummaryPage from './executive-summary'; + +const DeliveryCommandPage: any = ExecutiveSummaryPage; + +DeliveryCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout; + +export default DeliveryCommandPage; diff --git a/frontend/src/pages/executive-command.tsx b/frontend/src/pages/executive-command.tsx new file mode 100644 index 0000000..94c20a3 --- /dev/null +++ b/frontend/src/pages/executive-command.tsx @@ -0,0 +1,7 @@ +import ExecutiveSummaryPage from './executive-summary'; + +const ExecutiveCommandPage: any = ExecutiveSummaryPage; + +ExecutiveCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout; + +export default ExecutiveCommandPage; diff --git a/frontend/src/pages/executive-summary.tsx b/frontend/src/pages/executive-summary.tsx index b87f24a..7119192 100644 --- a/frontend/src/pages/executive-summary.tsx +++ b/frontend/src/pages/executive-summary.tsx @@ -12,6 +12,7 @@ import { } from '@mdi/js'; import axios from 'axios'; import Head from 'next/head'; +import { useRouter } from 'next/router'; import Link from 'next/link'; import React, { ReactElement, ReactNode, useEffect, useMemo, useState } from 'react'; import BaseButton from '../components/BaseButton'; @@ -23,6 +24,7 @@ import SectionMain from '../components/SectionMain'; import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; import { getWorkspaceConfig, + getWorkspaceRoute, type WorkspaceDetailBlockKey, type WorkspaceMetricKey, type WorkspaceQuickLinkIconKey, @@ -343,12 +345,22 @@ const SummaryMetric = ({ ); const ExecutiveSummaryPage = () => { + const router = useRouter(); const { currentUser } = useAppSelector((state) => state.auth); const [data, setData] = useState(defaultResponse); const [loading, setLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(''); + const workspaceRoute = useMemo(() => getWorkspaceRoute(currentUser?.app_role?.name), [currentUser?.app_role?.name]); const workspaceConfig = useMemo(() => getWorkspaceConfig(currentUser?.app_role?.name), [currentUser?.app_role?.name]); + useEffect(() => { + if (!currentUser?.id || router.pathname !== '/executive-summary' || workspaceRoute === '/executive-summary') { + return; + } + + router.replace(workspaceRoute); + }, [currentUser?.id, router, workspaceRoute]); + useEffect(() => { const fetchSummary = async () => { try { diff --git a/frontend/src/pages/financial-control.tsx b/frontend/src/pages/financial-control.tsx new file mode 100644 index 0000000..371839c --- /dev/null +++ b/frontend/src/pages/financial-control.tsx @@ -0,0 +1,7 @@ +import ExecutiveSummaryPage from './executive-summary'; + +const FinancialControlPage: any = ExecutiveSummaryPage; + +FinancialControlPage.getLayout = (ExecutiveSummaryPage as any).getLayout; + +export default FinancialControlPage; diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 35df893..983a691 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -12,13 +12,17 @@ import { import type { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; -import React from 'react'; +import { useRouter } from 'next/router'; +import React, { useEffect, useMemo } from 'react'; import BaseButton from '../components/BaseButton'; import BaseDivider from '../components/BaseDivider'; import BaseIcon from '../components/BaseIcon'; import CardBox from '../components/CardBox'; -import LayoutGuest from '../layouts/Guest'; import { getPageTitle } from '../config'; +import { getLoginRoute, getWorkspaceRoute } from '../helpers/workspace'; +import LayoutGuest from '../layouts/Guest'; +import { findMe } from '../stores/authSlice'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; const moduleCards = [ { @@ -51,6 +55,34 @@ const controlPoints = [ ]; export default function HomePage() { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { currentUser, token } = useAppSelector((state) => state.auth); + + const workspaceRoute = useMemo( + () => getWorkspaceRoute(currentUser?.app_role?.name), + [currentUser?.app_role?.name], + ); + + useEffect(() => { + const existingToken = token || (typeof window !== 'undefined' ? localStorage.getItem('token') : ''); + + if (existingToken && !currentUser?.id) { + dispatch(findMe()); + } + }, [currentUser?.id, dispatch, token]); + + useEffect(() => { + if (currentUser?.id) { + router.replace(workspaceRoute); + } + }, [currentUser?.id, router, workspaceRoute]); + + const getProtectedHref = (href: string) => (currentUser?.id ? href : getLoginRoute(href)); + + const primaryWorkspaceHref = currentUser?.id ? workspaceRoute : getLoginRoute('/executive-summary'); + const dashboardHref = currentUser?.id ? '/dashboard' : getLoginRoute('/dashboard'); + return ( <> @@ -65,8 +97,8 @@ export default function HomePage() {

FDSU ERP

- - + +
@@ -85,9 +117,9 @@ export default function HomePage() {

- - - + + +
@@ -112,7 +144,7 @@ export default function HomePage() {

Access

-

Public landing with links into the secured admin workspace

+

Public landing with role-aware sign-in routing into the secured workspace

@@ -159,10 +191,10 @@ export default function HomePage() {

Quick access

{[ - { href: '/executive-summary', icon: mdiWalletOutline, title: 'Executive summary', text: 'Operational overview, approval queue, risk panel, and rollout indicators.' }, - { href: '/requisitions/requisitions-list', icon: mdiClipboardClockOutline, title: 'Requisitions', text: 'Create, list, and review procurement requests.' }, - { href: '/contracts/contracts-list', icon: mdiFileDocumentOutline, title: 'Contracts', text: 'Open the contract register and milestone detail views.' }, - { href: '/vendors/vendors-list', icon: mdiCheckDecagramOutline, title: 'Vendor master', text: 'Access qualification, banking data, compliance, and related history.' }, + { href: currentUser?.id ? workspaceRoute : getLoginRoute('/executive-summary'), icon: mdiWalletOutline, title: 'Executive summary', text: 'Operational overview, approval queue, risk panel, and rollout indicators.' }, + { href: getProtectedHref('/requisitions/requisitions-list'), icon: mdiClipboardClockOutline, title: 'Requisitions', text: 'Create, list, and review procurement requests.' }, + { href: getProtectedHref('/contracts/contracts-list'), icon: mdiFileDocumentOutline, title: 'Contracts', text: 'Open the contract register and milestone detail views.' }, + { href: getProtectedHref('/vendors/vendors-list'), icon: mdiCheckDecagramOutline, title: 'Vendor master', text: 'Access qualification, banking data, compliance, and related history.' }, ].map((item) => (
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 0285176..c2f856f 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -21,6 +21,7 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks'; import Link from 'next/link'; import {toast, ToastContainer} from "react-toastify"; import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' +import { getPostLoginRoute } from '../helpers/workspace'; export default function Login() { const router = useRouter(); @@ -58,16 +59,18 @@ export default function Login() { }, []); // Fetch user data useEffect(() => { - if (token) { + const existingToken = token || (typeof window !== 'undefined' ? localStorage.getItem('token') : ''); + + if (existingToken) { dispatch(findMe()); } - }, [token, dispatch]); - // Redirect to dashboard if user is logged in + }, [dispatch, token]); + // Redirect to the intended deep link or the role workspace if user is logged in useEffect(() => { if (currentUser?.id) { - router.push('/dashboard'); + router.replace(getPostLoginRoute(currentUser?.app_role?.name, router.query.redirect)); } - }, [currentUser?.id, router]); + }, [currentUser?.app_role?.name, currentUser?.id, router, router.query.redirect]); // Show error message if there is one useEffect(() => { if (errorMessage){ diff --git a/frontend/src/pages/operations-command.tsx b/frontend/src/pages/operations-command.tsx new file mode 100644 index 0000000..243aa0f --- /dev/null +++ b/frontend/src/pages/operations-command.tsx @@ -0,0 +1,7 @@ +import ExecutiveSummaryPage from './executive-summary'; + +const OperationsCommandPage: any = ExecutiveSummaryPage; + +OperationsCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout; + +export default OperationsCommandPage; diff --git a/frontend/src/pages/platform-command.tsx b/frontend/src/pages/platform-command.tsx new file mode 100644 index 0000000..ecc59f8 --- /dev/null +++ b/frontend/src/pages/platform-command.tsx @@ -0,0 +1,7 @@ +import ExecutiveSummaryPage from './executive-summary'; + +const PlatformCommandPage: any = ExecutiveSummaryPage; + +PlatformCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout; + +export default PlatformCommandPage; diff --git a/frontend/src/pages/procurement-desk.tsx b/frontend/src/pages/procurement-desk.tsx new file mode 100644 index 0000000..c156a74 --- /dev/null +++ b/frontend/src/pages/procurement-desk.tsx @@ -0,0 +1,7 @@ +import ExecutiveSummaryPage from './executive-summary'; + +const ProcurementDeskPage: any = ExecutiveSummaryPage; + +ProcurementDeskPage.getLayout = (ExecutiveSummaryPage as any).getLayout; + +export default ProcurementDeskPage;