Autosave: 20260404-043544
This commit is contained in:
parent
4f12eae1aa
commit
8d94ffcf46
@ -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 = (
|
||||
<>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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<string, WorkspaceConfig> = {
|
||||
[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;
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
7
frontend/src/pages/compliance-desk.tsx
Normal file
7
frontend/src/pages/compliance-desk.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import ExecutiveSummaryPage from './executive-summary';
|
||||
|
||||
const ComplianceDeskPage: any = ExecutiveSummaryPage;
|
||||
|
||||
ComplianceDeskPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
|
||||
|
||||
export default ComplianceDeskPage;
|
||||
@ -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 (
|
||||
<SectionMain>
|
||||
<CardBox className='border border-slate-200 bg-white'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>Role workspace redirect</p>
|
||||
<h2 className='text-xl font-semibold text-slate-900'>This dashboard is reserved for Super Administrators and Administrators.</h2>
|
||||
<p className='text-sm text-slate-600'>Redirecting you to your role workspace now.</p>
|
||||
<div>
|
||||
<Link href={workspaceRoute} className='text-sm font-medium text-blue-700'>Go to workspace now</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{getPageTitle('Overview')}
|
||||
{getPageTitle(dashboardCopy.pageTitle)}
|
||||
</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton
|
||||
icon={icon.mdiChartTimelineVariant}
|
||||
title='Overview'
|
||||
title={dashboardCopy.pageTitle}
|
||||
main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<CardBox className='mb-6 border border-slate-200 bg-white'>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>{dashboardCopy.eyebrow}</p>
|
||||
<p className='mt-2 text-sm text-slate-600'>{dashboardCopy.description}</p>
|
||||
</CardBox>
|
||||
|
||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||
currentUser={currentUser}
|
||||
@ -151,7 +198,7 @@ const Dashboard = () => {
|
||||
{!!rolesWidgets.length &&
|
||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
||||
{`${widgetsRole?.role?.label || dashboardCopy.widgetLabelFallback}'s widgets`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
7
frontend/src/pages/delivery-command.tsx
Normal file
7
frontend/src/pages/delivery-command.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import ExecutiveSummaryPage from './executive-summary';
|
||||
|
||||
const DeliveryCommandPage: any = ExecutiveSummaryPage;
|
||||
|
||||
DeliveryCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
|
||||
|
||||
export default DeliveryCommandPage;
|
||||
7
frontend/src/pages/executive-command.tsx
Normal file
7
frontend/src/pages/executive-command.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import ExecutiveSummaryPage from './executive-summary';
|
||||
|
||||
const ExecutiveCommandPage: any = ExecutiveSummaryPage;
|
||||
|
||||
ExecutiveCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
|
||||
|
||||
export default ExecutiveCommandPage;
|
||||
@ -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<ExecutiveSummaryResponse>(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 {
|
||||
|
||||
7
frontend/src/pages/financial-control.tsx
Normal file
7
frontend/src/pages/financial-control.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import ExecutiveSummaryPage from './executive-summary';
|
||||
|
||||
const FinancialControlPage: any = ExecutiveSummaryPage;
|
||||
|
||||
FinancialControlPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
|
||||
|
||||
export default FinancialControlPage;
|
||||
@ -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 (
|
||||
<>
|
||||
<Head>
|
||||
@ -65,8 +97,8 @@ export default function HomePage() {
|
||||
<h1 className='mt-1 text-2xl font-semibold tracking-tight text-slate-950'>FDSU ERP</h1>
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center gap-3'>
|
||||
<BaseButton href='/login' color='whiteDark' icon={mdiLogin} label='Login' />
|
||||
<BaseButton href='/dashboard' color='info' icon={mdiOfficeBuildingCogOutline} label='Admin interface' />
|
||||
<BaseButton href={currentUser?.id ? workspaceRoute : '/login'} color='whiteDark' icon={mdiLogin} label={currentUser?.id ? 'My workspace' : 'Login'} />
|
||||
<BaseButton href={dashboardHref} color='info' icon={mdiOfficeBuildingCogOutline} label='Admin interface' />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -85,9 +117,9 @@ export default function HomePage() {
|
||||
</p>
|
||||
|
||||
<div className='mt-8 flex flex-wrap gap-3'>
|
||||
<BaseButton href='/executive-summary' color='info' label='Open executive summary' />
|
||||
<BaseButton href='/requisitions/requisitions-new' color='whiteDark' label='Create requisition' />
|
||||
<BaseButton href='/approvals/approvals-list' color='whiteDark' label='Open approval inbox' />
|
||||
<BaseButton href={primaryWorkspaceHref} color='info' label={currentUser?.id ? 'Open my workspace' : 'Open executive summary'} />
|
||||
<BaseButton href={getProtectedHref('/requisitions/requisitions-new')} color='whiteDark' label='Create requisition' />
|
||||
<BaseButton href={getProtectedHref('/approvals/approvals-list')} color='whiteDark' label='Open approval inbox' />
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
@ -112,7 +144,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div className='rounded-md border border-slate-200 p-4'>
|
||||
<p className='text-xs uppercase tracking-[0.18em] text-slate-400'>Access</p>
|
||||
<p className='mt-2 text-sm font-medium text-slate-900'>Public landing with links into the secured admin workspace</p>
|
||||
<p className='mt-2 text-sm font-medium text-slate-900'>Public landing with role-aware sign-in routing into the secured workspace</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
@ -159,10 +191,10 @@ export default function HomePage() {
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-500'>Quick access</p>
|
||||
<div className='mt-5 grid gap-3'>
|
||||
{[
|
||||
{ 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) => (
|
||||
<Link key={item.href} href={item.href} className='rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50'>
|
||||
<div className='flex items-start gap-3'>
|
||||
|
||||
@ -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){
|
||||
|
||||
7
frontend/src/pages/operations-command.tsx
Normal file
7
frontend/src/pages/operations-command.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import ExecutiveSummaryPage from './executive-summary';
|
||||
|
||||
const OperationsCommandPage: any = ExecutiveSummaryPage;
|
||||
|
||||
OperationsCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
|
||||
|
||||
export default OperationsCommandPage;
|
||||
7
frontend/src/pages/platform-command.tsx
Normal file
7
frontend/src/pages/platform-command.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import ExecutiveSummaryPage from './executive-summary';
|
||||
|
||||
const PlatformCommandPage: any = ExecutiveSummaryPage;
|
||||
|
||||
PlatformCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
|
||||
|
||||
export default PlatformCommandPage;
|
||||
7
frontend/src/pages/procurement-desk.tsx
Normal file
7
frontend/src/pages/procurement-desk.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import ExecutiveSummaryPage from './executive-summary';
|
||||
|
||||
const ProcurementDeskPage: any = ExecutiveSummaryPage;
|
||||
|
||||
ProcurementDeskPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
|
||||
|
||||
export default ProcurementDeskPage;
|
||||
Loading…
x
Reference in New Issue
Block a user