Autosave: 20260404-171444

This commit is contained in:
Flatlogic Bot 2026-04-04 17:14:44 +00:00
parent d7c816c260
commit 8ceeef1051
9 changed files with 957 additions and 2125 deletions

View File

@ -1,4 +1,5 @@
const express = require('express');
const validator = require('validator');
const db = require('../db/models');
const wrapAsync = require('../helpers').wrapAsync;
@ -67,6 +68,27 @@ const SUPPORTED_RECORD_TYPES = new Set([
'compliance_alerts',
]);
const MIN_MAJOR_CONTRACT_VALUE = 1000;
const hasMeaningfulText = (value) => Boolean(String(value || '').trim());
const getRecordId = (value) => {
const normalizedValue = String(value || '').trim();
if (!normalizedValue || !validator.isUUID(normalizedValue)) {
return null;
}
return normalizedValue;
};
const getUserDisplayName = (user, fallback = '') => {
if (!user) {
return fallback;
}
return `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.email || fallback;
};
const formatCurrencyValue = (value, currency) =>
new Intl.NumberFormat(currency === 'CDF' ? 'fr-CD' : 'en-US', {
@ -813,7 +835,11 @@ router.get(
]);
const provinceRolloutMap = rolloutProjects.reduce((accumulator, project) => {
const provinceName = project.province?.name || 'Province not assigned';
const provinceName = project.province?.name;
if (!provinceName) {
return accumulator;
}
if (!accumulator[provinceName]) {
accumulator[provinceName] = {
@ -847,23 +873,27 @@ router.get(
.slice(0, 6);
const formattedApprovalQueue = approvalQueue
.filter((approval) => SUPPORTED_RECORD_TYPES.has(approval.record_type))
.filter(
(approval) =>
SUPPORTED_RECORD_TYPES.has(approval.record_type)
&& hasMeaningfulText(approval.record_key)
&& hasMeaningfulText(approval.workflow?.name)
&& hasMeaningfulText(approval.step?.name)
&& Boolean(getUserDisplayName(approval.requested_by_user)),
)
.map((approval) => ({
id: approval.id,
recordType: approval.record_type,
recordKey: approval.record_key,
status: approval.status,
requestedAt: approval.requested_at,
workflowName: approval.workflow?.name || 'Workflow not configured',
stepName: approval.step?.name || 'Pending step setup',
stepOrder: approval.step?.step_order || null,
requestedBy: approval.requested_by_user
? `${approval.requested_by_user.firstName || ''} ${approval.requested_by_user.lastName || ''}`.trim() || approval.requested_by_user.email
: 'Requester unavailable',
assignedTo: approval.assigned_to_user
? `${approval.assigned_to_user.firstName || ''} ${approval.assigned_to_user.lastName || ''}`.trim() || approval.assigned_to_user.email
: 'Not assigned',
}));
id: approval.id,
recordType: approval.record_type,
recordKey: approval.record_key,
status: approval.status,
requestedAt: approval.requested_at,
recordId: getRecordId(approval.record_key),
workflowName: approval.workflow.name,
stepName: approval.step.name,
stepOrder: approval.step?.step_order || null,
requestedBy: getUserDisplayName(approval.requested_by_user),
assignedTo: getUserDisplayName(approval.assigned_to_user, 'Awaiting assignment'),
}));
const formattedProcurementQueue = procurementQueue.map((requisition) => ({
id: requisition.id,
@ -895,18 +925,23 @@ router.get(
}));
const formattedTopContracts = topContracts
.filter((contract) => contract.vendor?.name || contract.project?.name)
.filter(
(contract) =>
(contract.vendor?.name || contract.project?.name)
&& hasMeaningfulText(contract.contract_number || contract.title)
&& toNumber(contract.contract_value) >= MIN_MAJOR_CONTRACT_VALUE,
)
.map((contract) => ({
id: contract.id,
contractNumber: contract.contract_number,
title: contract.title,
contractValue: toNumber(contract.contract_value),
currency: contract.currency,
endDate: contract.end_date,
status: contract.status,
vendorName: contract.vendor?.name || 'Vendor not linked',
projectName: contract.project?.name || 'Project not linked',
}));
id: contract.id,
contractNumber: contract.contract_number,
title: contract.title,
contractValue: toNumber(contract.contract_value),
currency: contract.currency,
endDate: contract.end_date,
status: contract.status,
vendorName: contract.vendor?.name || 'Vendor not linked',
projectName: contract.project?.name || 'Project not linked',
}));
const formattedRiskPanel = riskPanel
.filter((alert) => !alert.record_type || SUPPORTED_RECORD_TYPES.has(alert.record_type))
@ -918,11 +953,10 @@ router.get(
details: alert.details,
recordType: alert.record_type,
recordKey: alert.record_key,
recordId: getRecordId(alert.record_key),
dueAt: alert.due_at,
status: alert.status,
assignedTo: alert.assigned_to_user
? `${alert.assigned_to_user.firstName || ''} ${alert.assigned_to_user.lastName || ''}`.trim() || alert.assigned_to_user.email
: 'Not assigned',
assignedTo: getUserDisplayName(alert.assigned_to_user, 'Unassigned'),
}));
const formattedNotifications = recentNotifications
@ -936,6 +970,7 @@ router.get(
sentAt: notification.sent_at,
recordType: notification.record_type,
recordKey: notification.record_key,
recordId: getRecordId(notification.record_key),
}));
const summary = {

View File

@ -19,7 +19,7 @@ export default function AsideMenu({
<>
<AsideMenuLayer
menu={props.menu}
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
className={`${isAsideMobileExpanded ? 'left-0' : '-left-72 lg:left-0'} ${
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
}`}
onAsideLgCloseClick={props.onAsideLgClose}

View File

@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react'
import { mdiMinus, mdiPlus } from '@mdi/js'
import BaseIcon from './BaseIcon'
import { mdiChevronDown, mdiChevronUp } from '@mdi/js'
import Link from 'next/link'
import { useRouter } from 'next/router'
import BaseIcon from './BaseIcon'
import { getButtonColor } from '../colors'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks'
import { useRouter } from 'next/router'
type Props = {
item: MenuAsideItem
@ -34,10 +34,8 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle)
const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle)
const asideMenuItemActiveStyle = useAppSelector((state) => state.style.asideMenuItemActiveStyle)
const borders = useAppSelector((state) => state.style.borders);
const activeLinkColor = useAppSelector(
(state) => state.style.activeLinkColor,
);
const borders = useAppSelector((state) => state.style.borders)
const activeLinkColor = useAppSelector((state) => state.style.activeLinkColor)
const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : ''
const isGroupTrigger = Boolean(item.menu && !item.href && !isDropdownList)
@ -62,56 +60,51 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const asideMenuItemInnerContents = (
<>
{item.icon && (
<BaseIcon path={item.icon} className={`flex-none mx-3 ${activeClassAddon}`} size="18" />
<span className='flex h-9 w-9 flex-none items-center justify-center rounded-xl'>
<BaseIcon path={item.icon} className={`${activeClassAddon}`} size='18' />
</span>
)}
<span
className={`grow text-ellipsis line-clamp-1 ${
item.menu ? '' : 'pr-12'
} ${isGroupTrigger ? 'text-[0.95rem] tracking-[0.01em]' : ''} ${activeClassAddon}`}
className={`min-w-0 grow whitespace-normal break-words leading-5 line-clamp-2 ${item.menu ? '' : 'pr-3'} ${isGroupTrigger ? 'text-[0.93rem] tracking-[0.01em]' : ''} ${activeClassAddon}`}
>
{item.label}
</span>
{item.menu && (
<BaseIcon
path={isDropdownActive ? mdiMinus : mdiPlus}
path={isDropdownActive ? mdiChevronUp : mdiChevronDown}
className={`flex-none ${activeClassAddon}`}
w="w-12"
size='18'
/>
)}
</>
)
const componentClass = [
'group flex items-center gap-1 rounded-xl border border-transparent px-3 transition-all duration-150',
'group flex w-full items-start gap-3 rounded-xl px-3 text-left transition-all duration-150',
isDropdownList ? 'py-2 text-sm' : 'py-2.5',
item.color
? getButtonColor(item.color, false, true)
: `${asideMenuItemStyle}`,
item.color ? getButtonColor(item.color, false, true) : `${asideMenuItemStyle}`,
isGroupTrigger ? 'font-semibold' : '',
isLinkActive
? `text-slate-950 ${activeLinkColor} shadow-sm ring-1 ring-inset ring-slate-200/80 dark:text-white dark:bg-slate-900 dark:ring-slate-800`
: 'hover:border-slate-200/80 dark:hover:border-slate-800/80',
].join(' ');
isLinkActive ? `${activeLinkColor} shadow-sm` : '',
].join(' ')
return (
<li className={isDropdownList ? 'px-3 py-1' : 'px-3 py-1.5'}>
<li className={isDropdownList ? 'px-2 py-1' : 'px-2 py-1.5'}>
{item.withDevider && <hr className={`${borders} mb-3`} />}
{item.href && (
{item.href ? (
<Link href={item.href} target={item.target} className={componentClass}>
{asideMenuItemInnerContents}
</Link>
)}
{!item.href && (
<div className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)}>
) : (
<button type='button' className={componentClass} onClick={() => setIsDropdownActive(!isDropdownActive)}>
{asideMenuItemInnerContents}
</div>
</button>
)}
{item.menu && (
<AsideMenuList
menu={item.menu}
className={`${asideMenuDropdownStyle} ${
isDropdownActive
? 'mt-2 ml-3 block rounded-xl border border-slate-200/70 pl-2 dark:border-slate-800 dark:bg-slate-900/40'
? 'mt-2 ml-3 block rounded-xl p-2'
: 'hidden'
}`}
isDropdownList

View File

@ -1,13 +1,12 @@
import React from 'react'
import { mdiLogout, mdiClose } from '@mdi/js'
import Link from 'next/link'
import axios from 'axios'
import { mdiArrowTopRight, mdiClose, mdiOfficeBuildingOutline } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Link from 'next/link';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { useAppSelector } from '../stores/hooks'
import { getWorkspaceConfig, getWorkspaceRoute } from '../helpers/workspace'
type Props = {
menu: MenuAsideItem[]
@ -15,75 +14,102 @@ type Props = {
onAsideLgCloseClick: () => void
}
type OrganizationOption = {
id: string
name: string
}
export default function AsideMenuLayer({ menu, className = '', ...props }: Props) {
const corners = useAppSelector((state) => state.style.corners);
const corners = useAppSelector((state) => state.style.corners)
const asideStyle = useAppSelector((state) => state.style.asideStyle)
const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle)
const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle)
const darkMode = useAppSelector((state) => state.style.darkMode)
const { currentUser } = useAppSelector((state) => state.auth)
const [organizations, setOrganizations] = React.useState<OrganizationOption[]>([])
const roleName = currentUser?.app_role?.name || ''
const workspaceConfig = getWorkspaceConfig(roleName)
const workspaceHref = getWorkspaceRoute(roleName)
const organizationId = currentUser?.organizations?.id
React.useEffect(() => {
let mounted = true
const fetchOrganizations = async () => {
try {
const response = await axios.get('/org-for-auth')
if (mounted) {
setOrganizations(Array.isArray(response.data) ? response.data : [])
}
} catch (error: any) {
console.error('Failed to load organizations for sidebar', error?.response || error)
}
}
fetchOrganizations()
return () => {
mounted = false
}
}, [])
const handleAsideLgCloseClick = (e: React.MouseEvent) => {
e.preventDefault()
props.onAsideLgCloseClick()
}
const dispatch = useAppDispatch();
const { currentUser } = useAppSelector((state) => state.auth);
const organizationsId = currentUser?.organizations?.id;
const [organizations, setOrganizations] = React.useState(null);
const organizationName = React.useMemo(() => {
const matchedOrganization = organizations.find((item) => item.id === organizationId)?.name
const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => {
try {
const response = await axios.get('/org-for-auth');
setOrganizations(response.data);
return response.data;
} catch (error) {
console.error(error.response);
throw error;
if (!matchedOrganization) {
return 'Organization workspace'
}
});
React.useEffect(() => {
dispatch(fetchOrganizations());
}, [dispatch]);
let organizationName = organizations?.find(item => item.id === organizationsId)?.name;
if(organizationName?.length > 25){
organizationName = organizationName?.substring(0, 25) + '...';
}
return matchedOrganization.length > 26 ? `${matchedOrganization.substring(0, 26)}...` : matchedOrganization
}, [organizationId, organizations])
return (
<aside
id='asideMenu'
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
className={`${className} fixed top-0 z-40 flex h-screen w-72 overflow-hidden transition-position lg:py-2 lg:pl-2`}
>
<div
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
>
<div
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
>
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
<b className="font-black">FDSU ERP</b>
{organizationName && <p>{organizationName}</p>}
<div className={`flex flex-1 flex-col overflow-hidden border ${asideStyle} ${corners}`}>
<div className={`border-b px-4 py-4 ${asideBrandStyle}`}>
<div className='flex items-start justify-between gap-3'>
<div className='min-w-0 flex-1'>
<p className='text-[11px] font-semibold uppercase tracking-[0.24em] text-slate-500 dark:text-slate-400'>FDSU ERP</p>
<h2 className='mt-2 line-clamp-2 text-lg font-semibold leading-6 text-slate-900 dark:text-white'>
{workspaceConfig.sidebarLabel}
</h2>
<div className='mt-3 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400'>
<BaseIcon path={mdiOfficeBuildingOutline} size={14} />
<span className='line-clamp-2 leading-5'>{organizationName}</span>
</div>
</div>
<button className='hidden rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 lg:inline-flex xl:hidden dark:hover:bg-slate-900 dark:hover:text-white' onClick={handleAsideLgCloseClick}>
<BaseIcon path={mdiClose} />
</button>
</div>
<div className='mt-4 border-t border-slate-200 pt-3 dark:border-slate-800'>
<p className='truncate text-sm font-semibold text-slate-900 dark:text-white'>
{currentUser?.firstName || currentUser?.email || 'Authenticated user'}
</p>
<div className='mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500 dark:text-slate-400'>
<span>{roleName || 'Workspace access'}</span>
<span aria-hidden='true'></span>
<Link href={workspaceHref} className='inline-flex items-center gap-1 font-medium text-blue-700 transition hover:text-slate-950 dark:text-sky-400 dark:hover:text-white'>
Open workspace home
<BaseIcon path={mdiArrowTopRight} size={14} />
</Link>
</div>
</div>
<button
className="hidden lg:inline-block xl:hidden p-3"
onClick={handleAsideLgCloseClick}
>
<BaseIcon path={mdiClose} />
</button>
</div>
<div
className={`flex-1 overflow-y-auto overflow-x-hidden ${
darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle
}`}
>
<div className={`flex-1 overflow-y-auto overflow-x-hidden px-2 py-3 ${darkMode ? 'aside-scrollbars-[slate]' : asideScrollbarsStyle}`}>
<AsideMenuList menu={menu} />
</div>
</div>

View File

@ -3,7 +3,7 @@ html {
}
body {
@apply pt-14 xl:pl-60 h-full;
@apply pt-14 xl:pl-72 h-full;
}
#app {

View File

@ -1,6 +1,7 @@
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import jwt from 'jsonwebtoken'
import { mdiBackburger, mdiForwardburger, mdiMenu } from '@mdi/js'
import { useRouter } from 'next/router'
import menuAside from '../menuAside'
import menuNavBar from '../menuNavBar'
import BaseIcon from '../components/BaseIcon'
@ -8,70 +9,61 @@ import NavBar from '../components/NavBar'
import NavBarItemPlain from '../components/NavBarItemPlain'
import AsideMenu from '../components/AsideMenu'
import FooterBar from '../components/FooterBar'
import Search from '../components/Search'
import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Search from '../components/Search';
import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice";
import {hasPermission} from "../helpers/userPermissions";
import { getLoginRoute } from "../helpers/workspace";
import { findMe, logoutUser } from '../stores/authSlice'
import { hasPermission } from '../helpers/userPermissions'
import { getLoginRoute } from '../helpers/workspace'
type Props = {
children: ReactNode
permission?: string
}
export default function LayoutAuthenticated({
children,
permission
}: Props) {
export default function LayoutAuthenticated({ children, permission }: Props) {
const dispatch = useAppDispatch()
const router = useRouter()
const { token, currentUser } = useAppSelector((state) => state.auth)
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
let localToken
if (typeof window !== 'undefined') {
// Perform localStorage action
localToken = localStorage.getItem('token')
}
const isTokenValid = () => {
const token = localStorage.getItem('token');
if (!token) return;
const date = new Date().getTime() / 1000;
const data = jwt.decode(token);
if (!data) return;
return date < data.exp;
};
useEffect(() => {
if (!isTokenValid()) {
dispatch(logoutUser());
router.replace(getLoginRoute(router.asPath));
return;
}
dispatch(findMe());
}, [dispatch, localToken, router, token]);
useEffect(() => {
if (!permission || !currentUser) return;
if (!hasPermission(currentUser, permission)) router.push('/error');
}, [currentUser, permission]);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor)
const darkMode = useAppSelector((state) => state.style.darkMode)
const [isAsideMobileExpanded, setIsAsideMobileExpanded] = useState(false)
const [isAsideLgActive, setIsAsideLgActive] = useState(false)
let localToken
if (typeof window !== 'undefined') {
localToken = localStorage.getItem('token')
}
const isTokenValid = () => {
const storedToken = localStorage.getItem('token')
if (!storedToken) return
const date = new Date().getTime() / 1000
const data: any = jwt.decode(storedToken)
if (!data) return
return date < data.exp
}
useEffect(() => {
if (!isTokenValid()) {
dispatch(logoutUser())
router.replace(getLoginRoute(router.asPath))
return
}
dispatch(findMe())
}, [dispatch, localToken, router, token])
useEffect(() => {
if (!permission || !currentUser) return
if (!hasPermission(currentUser, permission)) router.push('/error')
}, [currentUser, permission, router])
useEffect(() => {
const handleRouteChangeStart = () => {
setIsAsideMobileExpanded(false)
@ -80,51 +72,51 @@ export default function LayoutAuthenticated({
router.events.on('routeChangeStart', handleRouteChangeStart)
// If the component is unmounted, unsubscribe
// from the event with the `off` method:
return () => {
router.events.off('routeChangeStart', handleRouteChangeStart)
}
}, [router.events, dispatch])
}, [router.events])
const layoutAsidePadding = 'xl:pl-60'
const layoutAsidePadding = 'xl:pl-72'
return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
<div
className={`${layoutAsidePadding} ${
isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
} min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
>
<NavBar
menu={menuNavBar}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''}`}
>
<NavBarItemPlain
display="flex lg:hidden"
display='flex lg:hidden'
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
>
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size='24' />
</NavBarItemPlain>
<NavBarItemPlain
display="hidden lg:flex xl:hidden"
onClick={() => setIsAsideLgActive(true)}
>
<BaseIcon path={mdiMenu} size="24" />
<NavBarItemPlain display='hidden lg:flex xl:hidden' onClick={() => setIsAsideLgActive(true)}>
<BaseIcon path={mdiMenu} size='24' />
</NavBarItemPlain>
<NavBarItemPlain useMargin>
<Search />
</NavBarItemPlain>
</NavBar>
<AsideMenu
isAsideMobileExpanded={isAsideMobileExpanded}
isAsideLgActive={isAsideLgActive}
menu={menuAside}
onAsideLgClose={() => setIsAsideLgActive(false)}
/>
{children}
<FooterBar>Hand-crafted & Made with </FooterBar>
<div className='pt-14'>
<div className='min-h-[calc(100vh-3.5rem)]'>
{children}
</div>
<FooterBar />
</div>
</div>
</div>
)

View File

@ -10,13 +10,6 @@ const optionalIcon = (name: string, fallback: string = icon.mdiTable): string =>
const superAdminWorkspaceRoles = [WORKSPACE_ROLES.superAdmin]
const adminWorkspaceRoles = [WORKSPACE_ROLES.administrator]
const sharedEntityRoles = [
WORKSPACE_ROLES.directorGeneral,
WORKSPACE_ROLES.financeDirector,
WORKSPACE_ROLES.procurementLead,
WORKSPACE_ROLES.complianceAuditLead,
WORKSPACE_ROLES.projectDeliveryLead,
]
const superAdminGroupedNavigation: MenuAsideItem[] = [
{
@ -58,7 +51,7 @@ const superAdminGroupedNavigation: MenuAsideItem[] = [
],
},
{
label: 'Platform Workflow',
label: 'Workflow & Oversight',
icon: icon.mdiSitemap,
roles: superAdminWorkspaceRoles,
menu: [
@ -92,10 +85,41 @@ const superAdminGroupedNavigation: MenuAsideItem[] = [
const adminGroupedNavigation: MenuAsideItem[] = [
{
label: 'Workflow Readiness',
icon: icon.mdiSitemap,
label: 'Organization Setup',
icon: icon.mdiAccountGroup,
roles: adminWorkspaceRoles,
withDevider: true,
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',
},
{
href: '/documents/documents-list',
label: 'Documents',
icon: optionalIcon('mdiFolderFile'),
permissions: 'READ_DOCUMENTS',
},
],
},
{
label: 'Workflow Control',
icon: icon.mdiSitemap,
roles: adminWorkspaceRoles,
menu: [
{
href: '/approval_workflows/approval_workflows-list',
@ -124,7 +148,7 @@ const adminGroupedNavigation: MenuAsideItem[] = [
],
},
{
label: 'Operational Notices',
label: 'Assurance & Notices',
icon: icon.mdiBellOutline,
roles: adminWorkspaceRoles,
menu: [
@ -134,293 +158,17 @@ const adminGroupedNavigation: MenuAsideItem[] = [
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',
href: '/audit_logs/audit_logs-list',
label: 'Audit logs',
icon: optionalIcon('mdiClipboardTextClock'),
permissions: 'READ_AUDIT_LOGS',
},
],
},
@ -428,7 +176,6 @@ const adminGroupedNavigation: MenuAsideItem[] = [
const sharedEntityNavigation: MenuAsideItem[] = []
const menuAside: MenuAsideItem[] = [
{
href: '/executive-summary',
@ -447,7 +194,7 @@ const menuAside: MenuAsideItem[] = [
{
href: '/dashboard',
icon: icon.mdiViewDashboardOutline,
label: 'Admin Widgets',
label: 'Role Widgets',
labelByRole: {
[WORKSPACE_ROLES.superAdmin]: 'Platform Widgets',
[WORKSPACE_ROLES.administrator]: 'Operations Widgets',

File diff suppressed because it is too large Load Diff

View File

@ -64,6 +64,7 @@ interface ApprovalItem {
id: string;
recordType: string;
recordKey: string;
recordId?: string | null;
status: string;
requestedAt: string;
workflowName: string;
@ -106,6 +107,7 @@ interface RiskItem {
details: string;
recordType: string;
recordKey: string;
recordId?: string | null;
dueAt: string;
status: string;
assignedTo: string;
@ -127,6 +129,7 @@ interface NotificationItem {
sentAt: string;
recordType: string;
recordKey: string;
recordId?: string | null;
}
interface WorkspaceFocusCard {
@ -232,6 +235,47 @@ const formatCurrency = (value: number, currency: 'USD' | 'CDF') =>
maximumFractionDigits: 0,
}).format(value || 0);
const formatAbsoluteCurrency = (value: number, currency: 'USD' | 'CDF') => formatCurrency(Math.abs(value || 0), currency);
const getBudgetPositionMeta = (value: number, currency: 'USD' | 'CDF') => {
if (value < 0) {
return {
currency,
label: 'Overcommitted',
amount: formatAbsoluteCurrency(value, currency),
note: 'Live commitments are above approved allocations.',
containerClassName: 'border-red-200 bg-red-50',
amountClassName: 'text-red-950',
noteClassName: 'text-red-700',
badgeClassName: 'border-red-200 bg-white text-red-700',
};
}
if (value > 0) {
return {
currency,
label: 'Headroom',
amount: formatAbsoluteCurrency(value, currency),
note: 'Approved allocations still exceed live commitments.',
containerClassName: 'border-emerald-200 bg-emerald-50',
amountClassName: 'text-emerald-950',
noteClassName: 'text-emerald-700',
badgeClassName: 'border-emerald-200 bg-white text-emerald-700',
};
}
return {
currency,
label: 'On plan',
amount: formatCurrency(0, currency),
note: 'Approved allocations and live commitments are aligned.',
containerClassName: 'border-slate-200 bg-slate-50',
amountClassName: 'text-slate-900 dark:text-white',
noteClassName: 'text-slate-600 dark:text-slate-300',
badgeClassName: 'border-slate-200 bg-white text-slate-600 dark:text-slate-300',
};
};
const formatDate = (value?: string | null) => {
if (!value) {
return '—';
@ -286,29 +330,38 @@ const severityBadgeClass = (severity?: string) => {
}
};
const getRecordLink = (recordType?: string, recordKey?: string) => {
if (!recordType || !recordKey) {
const allowedDetailPages = new Set([
'requisitions',
'contracts',
'projects',
'vendors',
'tenders',
'awards',
'invoices',
'payment_requests',
'budget_reallocations',
'grants',
'compliance_alerts',
]);
const getRecordListLink = (recordType?: string) => {
if (!recordType || !allowedDetailPages.has(recordType)) {
return '/approvals/approvals-list';
}
const allowedDetailPages = new Set([
'requisitions',
'contracts',
'projects',
'vendors',
'tenders',
'awards',
'invoices',
'payment_requests',
'budget_reallocations',
'grants',
]);
return `/${recordType}/${recordType}-list`;
};
if (!allowedDetailPages.has(recordType)) {
const getRecordLink = (recordType?: string, recordId?: string | null) => {
if (!recordType || !allowedDetailPages.has(recordType)) {
return '/approvals/approvals-list';
}
return `/${recordType}/${recordType}-view?id=${recordKey}`;
if (!recordId) {
return getRecordListLink(recordType);
}
return `/${recordType}/${recordType}-view?id=${recordId}`;
};
const SectionHeader = ({
@ -320,7 +373,7 @@ const SectionHeader = ({
title: string;
action?: ReactNode;
}) => (
<div className='flex items-center justify-between gap-4 border-b border-slate-200 pb-4 dark:border-slate-800'>
<div className='flex flex-col gap-3 border-b border-slate-200 pb-4 md:flex-row md:items-end md:justify-between dark:border-slate-800'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>{eyebrow}</p>
<h3 className='mt-1 text-xl font-semibold text-slate-900 dark:text-white'>{title}</h3>
@ -340,14 +393,14 @@ const SummaryMetric = ({
note: string;
icon: string;
}) => (
<CardBox className='h-full border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:shadow-none'>
<CardBox className='h-full border border-slate-200 bg-white/90 shadow-sm shadow-slate-950/5 dark:border-slate-800 dark:bg-slate-900 dark:shadow-none'>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>{title}</p>
<p className='mt-3 text-2xl font-semibold text-slate-900 dark:text-white'>{value}</p>
<p className='mt-2 text-sm text-slate-500 dark:text-slate-400'>{note}</p>
</div>
<div className='flex h-11 w-11 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-800'>
<div className='flex h-11 w-11 items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-800'>
<BaseIcon path={icon} size={22} className='text-slate-700 dark:text-slate-200' />
</div>
</div>
@ -398,14 +451,14 @@ const RoleBriefingCard = ({
const style = getBriefingCardStyle(title);
return (
<CardBox className='h-full border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:shadow-none'>
<CardBox className='h-full border border-slate-200 bg-white/90 shadow-sm shadow-slate-950/5 dark:border-slate-800 dark:bg-slate-900 dark:shadow-none'>
<div className='flex h-full flex-col gap-4'>
<div className='flex items-start justify-between gap-3'>
<div>
<span className={`inline-flex rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] ${style.badgeClass}`}>{title}</span>
<p className='mt-3 text-base font-semibold text-slate-900 dark:text-white'>Responsibility brief</p>
</div>
<div className={`flex h-11 w-11 items-center justify-center rounded-xl border ${style.iconClass}`}>
<div className={`flex h-11 w-11 items-center justify-center rounded-2xl border ${style.iconClass}`}>
<BaseIcon path={style.icon} size={20} />
</div>
</div>
@ -590,6 +643,17 @@ const ExecutiveSummaryPage = () => {
const workspaceBriefingCards = workspaceConfig.briefingCards.slice(0, 4);
const receivesFromCard = workspaceBriefingCards.find((card) => card.title.toLowerCase().includes('receive'));
const handsOffCard = workspaceBriefingCards.find((card) => card.title.toLowerCase().includes('hands off'));
const hiddenApprovalCount = useMemo(
() => Math.max((data.summary.pendingApprovals || 0) - data.approvalQueue.length, 0),
[data.approvalQueue.length, data.summary.pendingApprovals],
);
const budgetPositionCards = useMemo(
() => ([
getBudgetPositionMeta(data.summary.budgetVariance.USD, 'USD'),
getBudgetPositionMeta(data.summary.budgetVariance.CDF, 'CDF'),
]),
[data.summary.budgetVariance.CDF, data.summary.budgetVariance.USD],
);
const focusBlock = Boolean(data.workspace?.focusCards?.length) && (
<div className='mb-6 grid grid-cols-1 gap-4 xl:grid-cols-4'>
@ -660,8 +724,14 @@ const ExecutiveSummaryPage = () => {
{loading ? (
<div className='py-10 text-sm text-slate-500 dark:text-slate-400'>Loading approval workload</div>
) : data.approvalQueue.length ? (
<div className='mt-4 overflow-x-auto'>
<table className='min-w-full divide-y divide-slate-200 text-sm'>
<>
{hiddenApprovalCount ? (
<div className='mt-4 rounded-md border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900'>
{hiddenApprovalCount} pending approval{hiddenApprovalCount === 1 ? '' : 's'} {hiddenApprovalCount === 1 ? 'is' : 'are'} hidden from this queue because workflow or record routing details are incomplete. Complete the underlying setup to make {hiddenApprovalCount === 1 ? 'it' : 'them'} operational.
</div>
) : null}
<div className='mt-4 overflow-x-auto'>
<table className='min-w-full divide-y divide-slate-200 text-sm'>
<thead>
<tr className='text-left text-xs uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400'>
<th className='py-3 pr-4 font-semibold'>Workflow</th>
@ -694,7 +764,7 @@ const ExecutiveSummaryPage = () => {
<Link href={`/approvals/approvals-view?id=${item.id}`} className='font-medium text-blue-700 hover:text-blue-900'>
Open approval
</Link>
<Link href={getRecordLink(item.recordType, item.recordKey)} className='text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:text-white'>
<Link href={getRecordLink(item.recordType, item.recordId)} className='text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:text-white'>
{humanize(item.recordType)} record
</Link>
</div>
@ -702,7 +772,12 @@ const ExecutiveSummaryPage = () => {
</tr>
))}
</tbody>
</table>
</table>
</div>
</>
) : hiddenApprovalCount ? (
<div className='mt-5 rounded-md border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900'>
{hiddenApprovalCount} pending approval{hiddenApprovalCount === 1 ? '' : 's'} are not yet routable because workflow or requester setup is incomplete. Complete the approval setup to move them into the active queue.
</div>
) : (
<div className='mt-5 rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500 dark:text-slate-400 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400'>
@ -723,12 +798,21 @@ const ExecutiveSummaryPage = () => {
/>
<div className='mt-4 grid gap-3'>
<div className='rounded-md border border-red-100 bg-red-50 p-4 dark:border-red-900/60 dark:bg-red-950/40'>
<p className='text-sm font-semibold text-red-800'>Budget headroom</p>
<p className='mt-2 text-lg font-semibold text-red-950'>
{formatCurrency(data.summary.budgetVariance.USD, 'USD')} / {formatCurrency(data.summary.budgetVariance.CDF, 'CDF')}
</p>
<p className='mt-1 text-sm text-red-700'>Remaining variance between approved allocations and current commitments.</p>
<div className='rounded-md border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/70'>
<p className='text-sm font-semibold text-slate-900 dark:text-white'>Budget position</p>
<p className='mt-1 text-sm text-slate-600 dark:text-slate-300'>Difference between approved allocations and live commitments by currency.</p>
<div className='mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2'>
{budgetPositionCards.map((item) => (
<div key={item.currency} className={`rounded-md border p-4 ${item.containerClassName}`}>
<div className='flex items-center justify-between gap-3'>
<p className='text-xs font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400'>{item.currency}</p>
<span className={`inline-flex rounded-md border px-2 py-1 text-xs font-semibold ${item.badgeClassName}`}>{item.label}</span>
</div>
<p className={`mt-3 text-lg font-semibold ${item.amountClassName}`}>{item.amount}</p>
<p className={`mt-1 text-sm ${item.noteClassName}`}>{item.note}</p>
</div>
))}
</div>
</div>
<div className='grid grid-cols-2 gap-3'>
<div className='rounded-md border border-amber-200 bg-amber-50 p-4 dark:border-amber-900/60 dark:bg-amber-950/30'>
@ -745,7 +829,7 @@ const ExecutiveSummaryPage = () => {
<div className='mt-5 space-y-3'>
{data.riskPanel.length ? (
data.riskPanel.slice(0, 4).map((risk) => (
<Link href={getRecordLink(risk.recordType, risk.recordKey)} key={risk.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50 dark:border-slate-800 dark:hover:border-slate-700 dark:hover:bg-slate-800/70'>
<Link href={getRecordLink(risk.recordType, risk.recordId)} key={risk.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50 dark:border-slate-800 dark:hover:border-slate-700 dark:hover:bg-slate-800/70'>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='font-medium text-slate-900 dark:text-white'>{risk.title}</p>
@ -900,7 +984,7 @@ const ExecutiveSummaryPage = () => {
<div className='mt-4 space-y-3'>
{data.recentNotifications.length ? (
data.recentNotifications.map((notification) => (
<Link href={getRecordLink(notification.recordType, notification.recordKey)} key={notification.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50 dark:border-slate-800 dark:hover:border-slate-700 dark:hover:bg-slate-800/70'>
<Link href={getRecordLink(notification.recordType, notification.recordId)} key={notification.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50 dark:border-slate-800 dark:hover:border-slate-700 dark:hover:bg-slate-800/70'>
<div className='flex items-start justify-between gap-3'>
<div>
<p className='font-medium text-slate-900 dark:text-white'>{notification.title}</p>
@ -961,7 +1045,7 @@ const ExecutiveSummaryPage = () => {
))
) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500 dark:text-slate-400 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400'>
No provincial rollout data is available yet.
No provincial rollout is available yet. Assign provinces to projects to unlock delivery coverage by province.
</div>
)}
</div>
@ -1008,7 +1092,7 @@ const ExecutiveSummaryPage = () => {
</table>
) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500 dark:text-slate-400 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-400'>
No contract commitments are available yet.
No material contract commitments are ready for executive review yet. Major commitments appear here once contracts have linked delivery context and a meaningful registered value.
</div>
)}
</div>
@ -1072,59 +1156,59 @@ const ExecutiveSummaryPage = () => {
</BaseButtons>
</SectionTitleLineWithButton>
<CardBox className='mb-6 overflow-hidden border border-slate-200 bg-gradient-to-br from-slate-950 via-slate-900 to-slate-800 text-slate-100 shadow-sm'>
<div className='grid gap-6 lg:grid-cols-[1.7fr,1fr]'>
<CardBox className='mb-6 overflow-hidden border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900'>
<div className='grid gap-6 lg:grid-cols-[1.7fr,1fr] lg:items-start'>
<div>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-400 dark:text-slate-500'>{workspaceConfig.eyebrow}</p>
<h2 className='mt-3 text-3xl font-semibold tracking-tight text-white'>{workspaceConfig.heroTitle}</h2>
<p className='mt-4 max-w-3xl text-sm leading-6 text-slate-300'>{workspaceConfig.heroDescription}</p>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-500 dark:text-slate-400'>{workspaceConfig.eyebrow}</p>
<h2 className='mt-3 text-3xl font-semibold tracking-tight text-slate-900 dark:text-white'>{workspaceConfig.heroTitle}</h2>
<p className='mt-4 max-w-3xl text-sm leading-6 text-slate-600 dark:text-slate-300'>{workspaceConfig.heroDescription}</p>
<div className='mt-6 grid gap-3 sm:grid-cols-2'>
{heroMetricChips.map((metric) => (
<div key={metric.title} className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
<p className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500'>{metric.title}</p>
<p className='mt-2 text-2xl font-semibold text-white'>{metric.value}</p>
<p className='mt-2 text-xs leading-5 text-slate-400 dark:text-slate-500'>{metric.note}</p>
<div key={metric.title} className='rounded-xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-800/70'>
<p className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>{metric.title}</p>
<p className='mt-2 text-2xl font-semibold text-slate-950 dark:text-white'>{metric.value}</p>
<p className='mt-2 text-xs leading-5 text-slate-500 dark:text-slate-400'>{metric.note}</p>
</div>
))}
</div>
</div>
<div className='grid gap-4 text-sm'>
<div className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500'>Logged-in role</p>
<p className='mt-2 text-lg font-semibold text-white'>{currentUser?.firstName || currentUser?.email || 'Authenticated user'}</p>
<p className='mt-1 text-slate-400 dark:text-slate-500'>{data.workspace?.roleName || currentUser?.app_role?.name || 'Operational access'}</p>
<div className='rounded-xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-800/70'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400'>Logged-in role</p>
<p className='mt-2 text-lg font-semibold text-slate-950 dark:text-white'>{currentUser?.firstName || currentUser?.email || 'Authenticated user'}</p>
<p className='mt-1 text-slate-500 dark:text-slate-400'>{data.workspace?.roleName || currentUser?.app_role?.name || 'Operational access'}</p>
<div className='mt-4 flex flex-wrap gap-2'>
<span className='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300'>
<span className='rounded-full border border-slate-200 bg-white px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300'>
{workspaceConfig.quickLinks.length} quick links
</span>
<span className='rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-300'>
<span className='rounded-full border border-slate-200 bg-white px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300'>
{workspaceConfig.blockOrder.length} focus sections
</span>
</div>
</div>
<div className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500'>Role interconnection</p>
<div className='rounded-xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-800/70'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400'>Role interconnection</p>
<div className='mt-4 space-y-4'>
<div>
<p className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>{receivesFromCard?.title || 'Receives from'}</p>
<p className='mt-2 text-sm leading-6 text-slate-300'>{receivesFromCard?.items?.[0] || 'Who this role receives work and information from.'}</p>
<p className='mt-2 text-sm leading-6 text-slate-600 dark:text-slate-300'>{receivesFromCard?.items?.[0] || 'Who this role receives work and information from.'}</p>
</div>
<div>
<p className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>{handsOffCard?.title || 'Hands off to'}</p>
<p className='mt-2 text-sm leading-6 text-slate-300'>{handsOffCard?.items?.[0] || "Who depends on this role's decisions and follow-through."}</p>
<p className='mt-2 text-sm leading-6 text-slate-600 dark:text-slate-300'>{handsOffCard?.items?.[0] || "Who depends on this role's decisions and follow-through."}</p>
</div>
</div>
</div>
<div className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500'>Institution signals</p>
<div className='rounded-xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-800/70'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-slate-400'>Institution signals</p>
<div className='mt-3 grid grid-cols-2 gap-3'>
<div>
<p className='text-2xl font-semibold text-white'>{data.summary.overduePayments}</p>
<p className='text-slate-400 dark:text-slate-500'>Overdue payment requests</p>
<p className='text-2xl font-semibold text-slate-950 dark:text-white'>{data.summary.overduePayments}</p>
<p className='text-slate-500 dark:text-slate-400'>Overdue payment requests</p>
</div>
<div>
<p className='text-2xl font-semibold text-white'>{data.summary.unreadNotifications}</p>
<p className='text-slate-400 dark:text-slate-500'>Unread notifications</p>
<p className='text-2xl font-semibold text-slate-950 dark:text-white'>{data.summary.unreadNotifications}</p>
<p className='text-slate-500 dark:text-slate-400'>Unread notifications</p>
</div>
</div>
</div>