Autosave: 20260404-043544

This commit is contained in:
Flatlogic Bot 2026-04-04 04:35:44 +00:00
parent 4f12eae1aa
commit 8d94ffcf46
16 changed files with 1008 additions and 484 deletions

View File

@ -13,6 +13,20 @@ type Props = {
isDropdownList?: boolean 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 AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const [isLinkActive, setIsLinkActive] = useState(false) const [isLinkActive, setIsLinkActive] = useState(false)
const [isDropdownActive, setIsDropdownActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false)
@ -29,16 +43,20 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const { asPath, isReady } = useRouter() const { asPath, isReady } = useRouter()
useEffect(() => { useEffect(() => {
if (item.href && isReady) { if (!isReady) {
const linkPathName = new URL(item.href, location.href).pathname + '/'; return
const activePathname = new URL(asPath, location.href).pathname
const activeView = activePathname.split('/')[1];
const linkPathNameView = linkPathName.split('/')[1];
setIsLinkActive(linkPathNameView === activeView);
} }
}, [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 = ( const asideMenuItemInnerContents = (
<> <>

View File

@ -3,7 +3,7 @@ import { MenuAsideItem } from '../interfaces'
import AsideMenuItem from './AsideMenuItem' import AsideMenuItem from './AsideMenuItem'
import { useAppSelector } from '../stores/hooks'; import { useAppSelector } from '../stores/hooks';
import { hasPermission } from '../helpers/userPermissions'; import { hasPermission } from '../helpers/userPermissions';
import { getWorkspaceConfig, itemVisibleForRole } from '../helpers/workspace'; import { getWorkspaceConfig, getWorkspaceRoute, itemVisibleForRole } from '../helpers/workspace';
type Props = { type Props = {
menu: MenuAsideItem[] menu: MenuAsideItem[]
@ -32,6 +32,7 @@ export default function AsideMenuList({ menu, isDropdownList = false, className
if (displayItem.label === 'Role Workspace') { if (displayItem.label === 'Role Workspace') {
displayItem.label = workspaceConfig.sidebarLabel; displayItem.label = workspaceConfig.sidebarLabel;
displayItem.href = getWorkspaceRoute(roleName);
} }
return ( return (

View File

@ -9,6 +9,18 @@ export const WORKSPACE_ROLES = {
public: 'Public', public: 'Public',
} as const; } 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 = export type WorkspaceMetricKey =
| 'approvedBudget' | 'approvedBudget'
| 'committedBudget' | 'committedBudget'
@ -197,114 +209,119 @@ const createWorkspaceConfig = ({
const workspaceConfigs: Record<string, WorkspaceConfig> = { const workspaceConfigs: Record<string, WorkspaceConfig> = {
[WORKSPACE_ROLES.superAdmin]: createWorkspaceConfig({ [WORKSPACE_ROLES.superAdmin]: createWorkspaceConfig({
sidebarLabel: 'Platform Command', sidebarLabel: 'Platform Command',
pageTitle: 'Super Administrator Workspace', pageTitle: 'Platform Command Workspace',
eyebrow: 'FDSU ERP · Super Administrator workspace', eyebrow: 'FDSU ERP · Tenant governance workspace',
heroTitle: 'Oversee tenants, access control, institutional activity, and platform-level risk from one command surface.', heroTitle: 'Govern tenants, access policy, institutional activity, and platform-level risk from one command surface.',
heroDescription: 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.', '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 organizations' }, primaryAction: { href: '/organizations/organizations-list', label: 'Review tenant registry' },
secondaryAction: { href: '/users/users-list', label: 'Manage users' }, secondaryAction: { href: '/users/users-list', label: 'Open access control' },
highlightedMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'contractsNearingExpiry', 'unreadNotifications', 'vendorComplianceAlerts'], highlightedMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'contractsNearingExpiry', 'unreadNotifications', 'vendorComplianceAlerts'],
heroMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'unreadNotifications'], heroMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'unreadNotifications'],
blockOrder: ['focus', 'watchlist', 'summary', 'approvalRisk', 'operations', 'delivery', 'actions'], blockOrder: ['focus', 'watchlist', 'summary', 'approvalRisk', 'operations', 'delivery', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Governance queue', eyebrow: 'Governance queue',
title: 'Approvals and escalations needing platform attention', title: 'Escalations and approvals requiring governance attention',
actionLabel: 'Open governance queue', actionLabel: 'Open governance queue',
}, },
riskPanel: { riskPanel: {
eyebrow: 'Cross-cutting control flags', eyebrow: 'Cross-cutting control flags',
title: 'Platform-wide exceptions and institutional exposure', title: 'Platform-wide exceptions, permission exposure, and institutional control risk',
}, },
recentNotifications: { recentNotifications: {
eyebrow: 'Platform activity', eyebrow: 'Cross-tenant activity',
title: 'Recent operational signals across tenants', title: 'Recent workflow and control signals across organizations',
actionLabel: 'Open activity feed', actionLabel: 'Open activity feed',
}, },
quickActions: { quickActions: {
eyebrow: 'Platform actions', eyebrow: 'Platform actions',
title: 'Jump directly into oversight and access-control work', title: 'Jump directly into tenant oversight and permission-control work',
}, },
}, },
quickLinks: [ quickLinks: [
{ {
href: '/organizations/organizations-list', href: '/organizations/organizations-list',
label: 'Organization registry', label: 'Organization registry',
description: 'Review tenant setup and coverage.', description: 'Review tenant setup, coverage, and stewardship ownership.',
icon: 'organizations', icon: 'organizations',
}, },
{ {
href: '/users/users-list', href: '/users/users-list',
label: 'User access', label: 'User access',
description: 'Manage accounts, roles, and ownership.', description: 'Manage accounts, authority assignments, and platform ownership.',
icon: 'users', icon: 'users',
}, },
{ {
href: '/approvals/approvals-list', href: '/roles/roles-list',
label: 'Escalation queue', label: 'Role catalog',
description: 'Follow stalled approvals and control breaks.', description: 'Govern platform-wide role definitions and responsibility boundaries.',
icon: 'approvals', icon: 'users',
}, },
{ {
href: '/audit_logs/audit_logs-list', href: '/permissions/permissions-list',
label: 'Audit trail', label: 'Permission matrix',
description: 'Trace administrative and ERP record changes.', description: 'Inspect and tighten permission coverage across the platform.',
icon: 'audit', icon: 'audit',
}, },
], ],
}), }),
[WORKSPACE_ROLES.administrator]: createWorkspaceConfig({ [WORKSPACE_ROLES.administrator]: createWorkspaceConfig({
sidebarLabel: 'Operations Command', sidebarLabel: 'Operations Command',
pageTitle: 'Administrator Workspace', pageTitle: 'Operations Command Workspace',
eyebrow: 'FDSU ERP · Administrator workspace', eyebrow: 'FDSU ERP · Workflow operations workspace',
heroTitle: 'Run tenant operations, workflow readiness, user support, and master data hygiene without losing ERP traceability.', heroTitle: 'Run workflow operations, user support, record readiness, and day-to-day administrative follow-through.',
heroDescription: heroDescription:
'This workspace is tuned for organization-level administration: approval setup, departments, notifications, operational bottlenecks, and administrative follow-through across the shared ERP.', '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: 'Review workflows' }, primaryAction: { href: '/approval_workflows/approval_workflows-list', label: 'Open workflow hub' },
secondaryAction: { href: '/notifications/notifications-list', label: 'Check notifications' }, secondaryAction: { href: '/notifications/notifications-list', label: 'Open notice center' },
highlightedMetricKeys: ['pendingApprovals', 'unreadNotifications', 'contractsNearingExpiry', 'openRiskAlerts', 'activeProjects', 'procurementPipeline'], highlightedMetricKeys: ['pendingApprovals', 'unreadNotifications', 'contractsNearingExpiry', 'openRiskAlerts', 'activeProjects', 'procurementPipeline'],
heroMetricKeys: ['pendingApprovals', 'unreadNotifications', 'procurementPipeline', 'contractsNearingExpiry'], heroMetricKeys: ['pendingApprovals', 'unreadNotifications', 'procurementPipeline', 'contractsNearingExpiry'],
blockOrder: ['focus', 'summary', 'approvalRisk', 'watchlist', 'operations', 'delivery', 'actions'], blockOrder: ['focus', 'summary', 'approvalRisk', 'watchlist', 'operations', 'delivery', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Operations queue', eyebrow: 'Operations queue',
title: 'Approvals waiting on workflow administration', title: 'Approvals waiting on routing, delegation, or admin follow-through',
actionLabel: 'Open workflow queue', actionLabel: 'Open workflow queue',
}, },
procurementQueue: { procurementQueue: {
eyebrow: 'Operational throughput', eyebrow: 'Operational throughput',
title: 'Requisitions that may require admin unblock', title: 'Requisitions that may require workflow or setup unblock',
actionLabel: 'Open requisitions', actionLabel: 'Open requisitions',
}, },
recentNotifications: {
eyebrow: 'Operational notices',
title: 'Recent admin-facing signals requiring follow-through',
actionLabel: 'Open notice center',
},
quickActions: { quickActions: {
eyebrow: 'Administrative actions', eyebrow: 'Administrative actions',
title: 'Go straight to configuration and operational follow-through', title: 'Open grouped control areas without scanning the full ERP register',
}, },
}, },
quickLinks: [ quickLinks: [
{ {
href: '/approval_workflows/approval_workflows-list', href: '/approval_workflows/approval_workflows-list',
label: 'Workflow setup', label: 'Workflow hub',
description: 'Review approval design and routing.', description: 'Keep approval routes, steps, and queues ready for daily use.',
icon: 'approvals', icon: 'approvals',
}, },
{ {
href: '/notifications/notifications-list', href: '/notifications/notifications-list',
label: 'Notification center', label: 'Notice center',
description: 'Resolve unread system and user signals.', description: 'Triage notifications, documents, and control signals in one stop.',
icon: 'notifications', icon: 'notifications',
}, },
{ {
href: '/users/users-list', href: '/users/users-list',
label: 'User support', label: 'Admin records',
description: 'Update access and account records.', description: 'Jump into users, departments, provinces, and support clean-up.',
icon: 'users', icon: 'users',
}, },
{ {
href: '/projects/projects-list', href: '/projects/projects-list',
label: 'Project register', label: 'Delivery register',
description: 'Verify record coverage and ownership.', description: 'Move from grouped navigation into active projects and contract follow-through.',
icon: 'projects', icon: 'projects',
}, },
], ],
@ -616,6 +633,54 @@ export function getWorkspaceConfig(roleName?: string | null): WorkspaceConfig {
return workspaceConfigs[roleName] || workspaceConfigs[WORKSPACE_ROLES.projectDeliveryLead]; 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) { export function itemVisibleForRole(itemRoles?: string[], roleName?: string | null) {
if (!itemRoles?.length) { if (!itemRoles?.length) {
return true; return true;

View File

@ -14,6 +14,7 @@ import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice"; import {findMe, logoutUser} from "../stores/authSlice";
import {hasPermission} from "../helpers/userPermissions"; import {hasPermission} from "../helpers/userPermissions";
import { getLoginRoute } from "../helpers/workspace";
type Props = { type Props = {
@ -49,12 +50,14 @@ export default function LayoutAuthenticated({
}; };
useEffect(() => { useEffect(() => {
dispatch(findMe());
if (!isTokenValid()) { if (!isTokenValid()) {
dispatch(logoutUser()); dispatch(logoutUser());
router.push('/login'); router.replace(getLoginRoute(router.asPath));
return;
} }
}, [token, localToken]);
dispatch(findMe());
}, [dispatch, localToken, router, token]);
useEffect(() => { useEffect(() => {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const ComplianceDeskPage: any = ExecutiveSummaryPage;
ComplianceDeskPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default ComplianceDeskPage;

View File

@ -1,16 +1,19 @@
import * as icon from '@mdi/js'; import * as icon from '@mdi/js';
import Head from 'next/head' import Head from 'next/head'
import { useRouter } from 'next/router'
import React from 'react' import React from 'react'
import axios from 'axios'; import axios from 'axios';
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import LayoutAuthenticated from '../layouts/Authenticated' import LayoutAuthenticated from '../layouts/Authenticated'
import SectionMain from '../components/SectionMain' import SectionMain from '../components/SectionMain'
import CardBox from '../components/CardBox'
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton' import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
import BaseIcon from "../components/BaseIcon"; import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config' import { getPageTitle } from '../config'
import Link from "next/link"; import Link from "next/link";
import { hasPermission } from "../helpers/userPermissions"; import { hasPermission } from "../helpers/userPermissions";
import { getWorkspaceRoute, isDashboardRole } from '../helpers/workspace';
import { fetchWidgets } from '../stores/roles/rolesSlice'; import { fetchWidgets } from '../stores/roles/rolesSlice';
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget'; import { SmartWidget } from '../components/SmartWidget/SmartWidget';
@ -18,6 +21,7 @@ import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useAppDispatch, useAppSelector } from '../stores/hooks';
const Dashboard = () => { const Dashboard = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const router = useRouter();
const iconsColor = useAppSelector((state) => state.style.iconsColor); const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners); const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
@ -83,7 +87,22 @@ const Dashboard = () => {
const { isFetchingQuery } = useAppSelector((state) => state.openAi); const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles); 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; const organizationId = currentUser?.organizations?.id;
@ -116,6 +135,12 @@ const Dashboard = () => {
async function getWidgets(roleId) { async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId)); await dispatch(fetchWidgets(roleId));
} }
React.useEffect(() => {
if (!currentUser?.id || dashboardAllowed) return;
router.replace(workspaceRoute);
}, [currentUser?.id, dashboardAllowed, router, workspaceRoute]);
React.useEffect(() => { React.useEffect(() => {
if (!currentUser) return; if (!currentUser) return;
loadData().then(); loadData().then();
@ -127,21 +152,43 @@ const Dashboard = () => {
getWidgets(widgetsRole?.role?.value || '').then(); getWidgets(widgetsRole?.role?.value || '').then();
}, [widgetsRole?.role?.value]); }, [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 ( return (
<> <>
<Head> <Head>
<title> <title>
{getPageTitle('Overview')} {getPageTitle(dashboardCopy.pageTitle)}
</title> </title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton <SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant} icon={icon.mdiChartTimelineVariant}
title='Overview' title={dashboardCopy.pageTitle}
main> main>
{''} {''}
</SectionTitleLineWithButton> </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 {hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser} currentUser={currentUser}
isFetchingQuery={isFetchingQuery} isFetchingQuery={isFetchingQuery}
@ -151,7 +198,7 @@ const Dashboard = () => {
{!!rolesWidgets.length && {!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && ( hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'> <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> </p>
)} )}

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const DeliveryCommandPage: any = ExecutiveSummaryPage;
DeliveryCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default DeliveryCommandPage;

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const ExecutiveCommandPage: any = ExecutiveSummaryPage;
ExecutiveCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default ExecutiveCommandPage;

View File

@ -12,6 +12,7 @@ import {
} from '@mdi/js'; } from '@mdi/js';
import axios from 'axios'; import axios from 'axios';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router';
import Link from 'next/link'; import Link from 'next/link';
import React, { ReactElement, ReactNode, useEffect, useMemo, useState } from 'react'; import React, { ReactElement, ReactNode, useEffect, useMemo, useState } from 'react';
import BaseButton from '../components/BaseButton'; import BaseButton from '../components/BaseButton';
@ -23,6 +24,7 @@ import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { import {
getWorkspaceConfig, getWorkspaceConfig,
getWorkspaceRoute,
type WorkspaceDetailBlockKey, type WorkspaceDetailBlockKey,
type WorkspaceMetricKey, type WorkspaceMetricKey,
type WorkspaceQuickLinkIconKey, type WorkspaceQuickLinkIconKey,
@ -343,12 +345,22 @@ const SummaryMetric = ({
); );
const ExecutiveSummaryPage = () => { const ExecutiveSummaryPage = () => {
const router = useRouter();
const { currentUser } = useAppSelector((state) => state.auth); const { currentUser } = useAppSelector((state) => state.auth);
const [data, setData] = useState<ExecutiveSummaryResponse>(defaultResponse); const [data, setData] = useState<ExecutiveSummaryResponse>(defaultResponse);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState(''); 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]); 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(() => { useEffect(() => {
const fetchSummary = async () => { const fetchSummary = async () => {
try { try {

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const FinancialControlPage: any = ExecutiveSummaryPage;
FinancialControlPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default FinancialControlPage;

View File

@ -12,13 +12,17 @@ import {
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; 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 BaseButton from '../components/BaseButton';
import BaseDivider from '../components/BaseDivider'; import BaseDivider from '../components/BaseDivider';
import BaseIcon from '../components/BaseIcon'; import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox'; import CardBox from '../components/CardBox';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config'; 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 = [ const moduleCards = [
{ {
@ -51,6 +55,34 @@ const controlPoints = [
]; ];
export default function HomePage() { 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 ( return (
<> <>
<Head> <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> <h1 className='mt-1 text-2xl font-semibold tracking-tight text-slate-950'>FDSU ERP</h1>
</div> </div>
<div className='flex flex-wrap items-center gap-3'> <div className='flex flex-wrap items-center gap-3'>
<BaseButton href='/login' color='whiteDark' icon={mdiLogin} label='Login' /> <BaseButton href={currentUser?.id ? workspaceRoute : '/login'} color='whiteDark' icon={mdiLogin} label={currentUser?.id ? 'My workspace' : 'Login'} />
<BaseButton href='/dashboard' color='info' icon={mdiOfficeBuildingCogOutline} label='Admin interface' /> <BaseButton href={dashboardHref} color='info' icon={mdiOfficeBuildingCogOutline} label='Admin interface' />
</div> </div>
</div> </div>
</header> </header>
@ -85,9 +117,9 @@ export default function HomePage() {
</p> </p>
<div className='mt-8 flex flex-wrap gap-3'> <div className='mt-8 flex flex-wrap gap-3'>
<BaseButton href='/executive-summary' color='info' label='Open executive summary' /> <BaseButton href={primaryWorkspaceHref} color='info' label={currentUser?.id ? 'Open my workspace' : 'Open executive summary'} />
<BaseButton href='/requisitions/requisitions-new' color='whiteDark' label='Create requisition' /> <BaseButton href={getProtectedHref('/requisitions/requisitions-new')} color='whiteDark' label='Create requisition' />
<BaseButton href='/approvals/approvals-list' color='whiteDark' label='Open approval inbox' /> <BaseButton href={getProtectedHref('/approvals/approvals-list')} color='whiteDark' label='Open approval inbox' />
</div> </div>
</div> </div>
</CardBox> </CardBox>
@ -112,7 +144,7 @@ export default function HomePage() {
</div> </div>
<div className='rounded-md border border-slate-200 p-4'> <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='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>
</div> </div>
</CardBox> </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> <p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-500'>Quick access</p>
<div className='mt-5 grid gap-3'> <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: currentUser?.id ? workspaceRoute : getLoginRoute('/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: getProtectedHref('/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: getProtectedHref('/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: getProtectedHref('/vendors/vendors-list'), icon: mdiCheckDecagramOutline, title: 'Vendor master', text: 'Access qualification, banking data, compliance, and related history.' },
].map((item) => ( ].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'> <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'> <div className='flex items-start gap-3'>

View File

@ -21,6 +21,7 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link'; import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
import { getPostLoginRoute } from '../helpers/workspace';
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
@ -58,16 +59,18 @@ export default function Login() {
}, []); }, []);
// Fetch user data // Fetch user data
useEffect(() => { useEffect(() => {
if (token) { const existingToken = token || (typeof window !== 'undefined' ? localStorage.getItem('token') : '');
if (existingToken) {
dispatch(findMe()); dispatch(findMe());
} }
}, [token, dispatch]); }, [dispatch, token]);
// Redirect to dashboard if user is logged in // Redirect to the intended deep link or the role workspace if user is logged in
useEffect(() => { useEffect(() => {
if (currentUser?.id) { 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 // Show error message if there is one
useEffect(() => { useEffect(() => {
if (errorMessage){ if (errorMessage){

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const OperationsCommandPage: any = ExecutiveSummaryPage;
OperationsCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default OperationsCommandPage;

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const PlatformCommandPage: any = ExecutiveSummaryPage;
PlatformCommandPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default PlatformCommandPage;

View File

@ -0,0 +1,7 @@
import ExecutiveSummaryPage from './executive-summary';
const ProcurementDeskPage: any = ExecutiveSummaryPage;
ProcurementDeskPage.getLayout = (ExecutiveSummaryPage as any).getLayout;
export default ProcurementDeskPage;