Autosave: 20260404-052155

This commit is contained in:
Flatlogic Bot 2026-04-04 05:21:56 +00:00
parent 8d94ffcf46
commit 4d4711eef8
7 changed files with 723 additions and 452 deletions

View File

@ -86,114 +86,114 @@ const getCollectionCount = (datasets, collectionKey) => {
const workspacePayloadConfigs = { const workspacePayloadConfigs = {
[WORKSPACE_ROLES.superAdmin]: { [WORKSPACE_ROLES.superAdmin]: {
summaryMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'contractsNearingExpiry', 'unreadNotifications', 'vendorComplianceAlerts'], summaryMetricKeys: ['pendingApprovals', 'openRiskAlerts', 'unreadNotifications', 'vendorComplianceAlerts', 'activeProjects', 'contractsNearingExpiry'],
focusCards: [ focusCards: [
{ {
key: 'super-admin-approvals', key: 'super-admin-approvals',
title: 'Workflow backlog requiring oversight', title: 'Governance approvals waiting attention',
metricKey: 'pendingApprovals', metricKey: 'pendingApprovals',
note: 'Pending approvals across the accessible ERP scope that may require escalation or reassignment.', note: 'Pending approvals visible to the Super Admin that may need reassignment, policy review, or escalation.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'super-admin-alerts', key: 'super-admin-risks',
title: 'Open risk exposure', title: 'Cross-organization control alerts',
metricKey: 'openRiskAlerts', metricKey: 'openRiskAlerts',
note: 'High and critical alerts still unresolved across contracts, vendors, and operational records.', note: 'Open high-priority alerts that may indicate access, compliance, or platform-governance problems.',
href: '/compliance_alerts/compliance_alerts-list', href: '/compliance_alerts/compliance_alerts-list',
}, },
{ {
key: 'super-admin-projects', key: 'super-admin-notifications',
title: 'Live execution footprint', title: 'Unread platform notices',
metricKey: 'activeProjects', metricKey: 'unreadNotifications',
note: 'Projects currently moving through the institution and contributing to workload volume.', note: 'Recent notices that can reveal governance, setup, or platform support issues.',
href: '/projects/projects-list', href: '/notifications/notifications-list',
}, },
{ {
key: 'super-admin-notifications', key: 'super-admin-vendor-alerts',
title: 'Unread notices', title: 'Vendor compliance exposure',
metricKey: 'unreadNotifications', metricKey: 'vendorComplianceAlerts',
note: 'Unread workflow, control, and system notices that may hide operational friction.', note: 'Supplier evidence and expiry issues that may require top-level visibility or policy follow-up.',
href: '/notifications/notifications-list', href: '/vendor_compliance_documents/vendor_compliance_documents-list',
}, },
], ],
watchlistCards: [ watchlistCards: [
{ {
key: 'super-admin-approval-queue', key: 'super-admin-approval-watch',
title: 'Escalation approval inbox', title: 'Governance approval queue',
collectionKey: 'approvalQueue', collectionKey: 'approvalQueue',
note: 'Oldest pending decisions visible from the platform oversight layer.', note: 'A quick read on pending decisions that are building institutional pressure.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'super-admin-risk-watch', key: 'super-admin-risk-watch',
title: 'Platform red-flag watchlist', title: 'Control red-flag watchlist',
collectionKey: 'riskPanel', collectionKey: 'riskPanel',
note: 'The most severe open alerts currently demanding follow-up.', note: 'The most severe open alerts currently demanding platform or executive follow-up.',
href: '/compliance_alerts/compliance_alerts-list', href: '/compliance_alerts/compliance_alerts-list',
}, },
{ {
key: 'super-admin-notification-watch', key: 'super-admin-notice-watch',
title: 'Unread institutional notices', title: 'Platform notice watchlist',
collectionKey: 'recentNotifications', collectionKey: 'recentNotifications',
note: 'Recent notifications that may indicate configuration or workflow issues.', note: 'Recent notices that can reveal workflow, access, or setup issues across organizations.',
href: '/notifications/notifications-list', href: '/notifications/notifications-list',
}, },
], ],
}, },
[WORKSPACE_ROLES.administrator]: { [WORKSPACE_ROLES.administrator]: {
summaryMetricKeys: ['pendingApprovals', 'unreadNotifications', 'contractsNearingExpiry', 'openRiskAlerts', 'activeProjects', 'procurementPipeline'], summaryMetricKeys: ['pendingApprovals', 'unreadNotifications', 'openRiskAlerts', 'vendorComplianceAlerts'],
focusCards: [ focusCards: [
{ {
key: 'admin-approvals', key: 'admin-approvals',
title: 'Approvals awaiting operational follow-through', title: 'Approvals blocked by routing or delegation',
metricKey: 'pendingApprovals', metricKey: 'pendingApprovals',
note: 'Work items that often surface workflow setup gaps, delegation issues, or stalled administrative action.', note: 'Items often delayed because workflow steps, assignments, or delegation setup need administrator attention.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'admin-notifications', key: 'admin-notifications',
title: 'Unread admin-facing notices', title: 'Unread administrator notices',
metricKey: 'unreadNotifications', metricKey: 'unreadNotifications',
note: 'Recent notifications that can point to user support, assignment, or setup problems.', note: 'Recent notices that can point to user support issues, record problems, or broken process follow-through.',
href: '/notifications/notifications-list', href: '/notifications/notifications-list',
}, },
{ {
key: 'admin-contracts', key: 'admin-risks',
title: 'Contracts nearing renewal attention', title: 'Administrative control signals',
metricKey: 'contractsNearingExpiry', metricKey: 'openRiskAlerts',
note: 'Active contracts nearing expiry and likely to trigger administrative coordination.', note: 'Open alerts that can indicate setup, delegation, or coordination problems affecting operations.',
href: '/contracts/contracts-list', href: '/compliance_alerts/compliance_alerts-list',
}, },
{ {
key: 'admin-pipeline', key: 'admin-vendor-alerts',
title: 'Procurement workload in motion', title: 'Evidence and document alerts',
metricKey: 'procurementPipeline', metricKey: 'vendorComplianceAlerts',
note: 'Requisitions advancing through review, tendering, or award that can pressure operations teams.', note: 'Document and compliance issues that often need routing, notice, or administrator coordination.',
href: '/requisitions/requisitions-list', href: '/vendor_compliance_documents/vendor_compliance_documents-list',
}, },
], ],
watchlistCards: [ watchlistCards: [
{ {
key: 'admin-approval-queue', key: 'admin-approval-watch',
title: 'Operational approval queue', title: 'Workflow routing queue',
collectionKey: 'approvalQueue', collectionKey: 'approvalQueue',
note: 'A quick read on which approvals need routing or user follow-up.', note: 'Approvals that may need route fixes, reassignment, or user follow-up.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'admin-procurement-queue', key: 'admin-notification-watch',
title: 'Live procurement queue', title: 'Administrator notice watchlist',
collectionKey: 'procurementQueue', collectionKey: 'recentNotifications',
note: 'Requisitions that may be blocked by setup, documentation, or coordination issues.', note: 'Recent support and system signals that help spot process issues early.',
href: '/requisitions/requisitions-list', href: '/notifications/notifications-list',
}, },
{ {
key: 'admin-notification-watch', key: 'admin-risk-watch',
title: 'Recent administrative notices', title: 'Administrative exception watchlist',
collectionKey: 'recentNotifications', collectionKey: 'riskPanel',
note: 'Recent events to help admins spot support and process issues early.', note: 'Open alerts and exceptions likely to require cleanup, coordination, or follow-through.',
href: '/notifications/notifications-list', href: '/compliance_alerts/compliance_alerts-list',
}, },
], ],
}, },
@ -201,112 +201,112 @@ const workspacePayloadConfigs = {
summaryMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects', 'contractsNearingExpiry'], summaryMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects', 'contractsNearingExpiry'],
focusCards: [ focusCards: [
{ {
key: 'dg-budget', key: 'dg-commitments',
title: 'Approved budget envelope', title: 'Committed budget requiring executive attention',
metricKey: 'approvedBudget', metricKey: 'committedBudget',
note: 'Current allocation envelope available for strategic oversight and institutional steering.', note: 'Current commitment pressure across the institution relative to approved funding space.',
href: '/allocations/allocations-list', href: '/allocations/allocations-list',
}, },
{ {
key: 'dg-disbursed', key: 'dg-approvals',
title: 'Disbursements already released', title: 'Executive decisions waiting',
metricKey: 'disbursedBudget', metricKey: 'pendingApprovals',
note: 'Payments processed so far, useful for comparing commitment and delivery momentum.', note: 'High-impact approvals and escalations still waiting for institutional direction.',
href: '/payments/payments-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'dg-risk-projects', key: 'dg-risks',
title: 'Projects under elevated risk', title: 'High-risk projects in view',
metricKey: 'highRiskProjects', metricKey: 'highRiskProjects',
note: 'Projects flagged high or critical risk and likely to require executive attention.', note: 'Projects marked high or critical risk and likely to need executive intervention.',
href: '/projects/projects-list', href: '/projects/projects-list',
}, },
{ {
key: 'dg-contracts', key: 'dg-contracts',
title: 'Contracts nearing decision point', title: 'Contracts nearing executive attention',
metricKey: 'contractsNearingExpiry', metricKey: 'contractsNearingExpiry',
note: 'Active contracts approaching expiry that may affect program continuity or exposure.', note: 'Active contracts approaching expiry and likely to need escalation, renewal, or decision.',
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
}, },
], ],
watchlistCards: [ watchlistCards: [
{ {
key: 'dg-contract-watch', key: 'dg-approval-watch',
title: 'Executive contract watchlist', title: 'Executive escalation queue',
collectionKey: 'contractWatchlist', collectionKey: 'approvalQueue',
note: 'Contracts closest to expiry or requiring strategic follow-up.', note: 'Pending approvals and requests most likely to require executive direction.',
href: '/contracts/contracts-list', href: '/approvals/approvals-list',
},
{
key: 'dg-rollout-watch',
title: 'Delivery footprint watchlist',
collectionKey: 'provinceRollout',
note: 'A quick read on execution spread and momentum across provinces.',
href: '/projects/projects-list',
}, },
{ {
key: 'dg-risk-watch', key: 'dg-risk-watch',
title: 'Strategic risk watchlist', title: 'Strategic risk watchlist',
collectionKey: 'riskPanel', collectionKey: 'riskPanel',
note: 'High-severity alerts that can affect institutional delivery credibility.', note: 'Open alerts and control issues that may affect institutional outcomes.',
href: '/compliance_alerts/compliance_alerts-list', href: '/compliance_alerts/compliance_alerts-list',
}, },
{
key: 'dg-rollout-watch',
title: 'Province rollout overview',
collectionKey: 'provinceRollout',
note: 'Delivery footprint by province to help spot uneven execution patterns.',
href: '/projects/projects-list',
},
], ],
}, },
[WORKSPACE_ROLES.financeDirector]: { [WORKSPACE_ROLES.financeDirector]: {
summaryMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments', 'pendingApprovals', 'unreadNotifications'], summaryMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments', 'pendingApprovals', 'unreadNotifications'],
focusCards: [ focusCards: [
{ {
key: 'finance-approved', key: 'finance-commitments',
title: 'Approved budget base', title: 'Committed budget under finance control',
metricKey: 'approvedBudget', metricKey: 'committedBudget',
note: 'The fiscal envelope currently approved and available for expenditure control.', note: 'Current commitments that finance must monitor against available funding and approval status.',
href: '/allocations/allocations-list', href: '/allocations/allocations-list',
}, },
{ {
key: 'finance-committed', key: 'finance-payments',
title: 'Committed demand',
metricKey: 'committedBudget',
note: 'Requisition value moving toward tender, award, or contract conversion.',
href: '/requisitions/requisitions-list',
},
{
key: 'finance-disbursed',
title: 'Disbursed amount',
metricKey: 'disbursedBudget',
note: 'Processed payments already released, useful for cash-control tracking.',
href: '/payments/payments-list',
},
{
key: 'finance-overdue',
title: 'Aging payment requests', title: 'Aging payment requests',
metricKey: 'overduePayments', metricKey: 'overduePayments',
note: 'Submitted or approved requests older than 30 days and likely creating vendor pressure.', note: 'Payment requests older than the review window and likely to create pressure on delivery or vendors.',
href: '/payment_requests/payment_requests-list', href: '/payment_requests/payment_requests-list',
}, },
{
key: 'finance-approvals',
title: 'Finance approvals waiting action',
metricKey: 'pendingApprovals',
note: 'Approvals slowing down commitments, disbursements, or other finance actions.',
href: '/approvals/approvals-list',
},
{
key: 'finance-disbursements',
title: 'Disbursed budget',
metricKey: 'disbursedBudget',
note: 'Processed payments already released and shaping the current cash position.',
href: '/ledger_entries/ledger_entries-list',
},
], ],
watchlistCards: [ watchlistCards: [
{
key: 'finance-payment-watch',
title: 'Payment control watchlist',
collectionKey: 'recentNotifications',
note: 'Recent notices that may affect settlement timing or finance follow-through.',
href: '/notifications/notifications-list',
},
{ {
key: 'finance-approval-watch', key: 'finance-approval-watch',
title: 'Approvals blocking spend', title: 'Finance approval queue',
collectionKey: 'approvalQueue', collectionKey: 'approvalQueue',
note: 'Pending approvals that can delay commitments, invoicing, or disbursement.', note: 'Pending approvals that are holding back commitments or disbursements.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'finance-contract-watch', key: 'finance-contract-watch',
title: 'Contract value watchlist', title: 'Commitment watchlist',
collectionKey: 'topContracts', collectionKey: 'contractWatchlist',
note: 'Largest contracts in scope for exposure review and budget tracking.', note: 'Contracts most likely to shape upcoming payment or budget pressure.',
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
}, },
{
key: 'finance-notification-watch',
title: 'Finance notice watchlist',
collectionKey: 'recentNotifications',
note: 'Recent notices that may signal bottlenecks in finance operations.',
href: '/notifications/notifications-list',
},
], ],
}, },
[WORKSPACE_ROLES.procurementLead]: { [WORKSPACE_ROLES.procurementLead]: {
@ -314,53 +314,53 @@ const workspacePayloadConfigs = {
focusCards: [ focusCards: [
{ {
key: 'procurement-pipeline', key: 'procurement-pipeline',
title: 'Live requisition pipeline', title: 'Requisitions moving through sourcing',
metricKey: 'procurementPipeline', metricKey: 'procurementPipeline',
note: 'Requisitions actively progressing through review, tender, and award stages.', note: 'Live demand currently progressing through review, tender, award, or conversion to contract.',
href: '/requisitions/requisitions-list', href: '/requisitions/requisitions-list',
}, },
{ {
key: 'procurement-approvals', key: 'procurement-approvals',
title: 'Approvals slowing procurement', title: 'Approvals slowing procurement',
metricKey: 'pendingApprovals', metricKey: 'pendingApprovals',
note: 'Pending workflow actions that can delay conversion from request to tender or contract.', note: 'Decisions delaying sourcing, evaluation, award, or contract activation.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'procurement-contracts', key: 'procurement-contracts',
title: 'Contracts close to expiry', title: 'Contracts nearing procurement follow-up',
metricKey: 'contractsNearingExpiry', metricKey: 'contractsNearingExpiry',
note: 'Active contracts approaching expiry and likely to require extension or replacement planning.', note: 'Active commitments approaching expiry and likely to require renewal, closure, or replacement.',
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
}, },
{ {
key: 'procurement-vendor-alerts', key: 'procurement-vendors',
title: 'Vendor compliance issues', title: 'Vendor readiness issues',
metricKey: 'vendorComplianceAlerts', metricKey: 'vendorComplianceAlerts',
note: 'Open missing-document or expiry alerts that may block award or execution readiness.', note: 'Supplier document and compliance gaps that can slow award or contract execution.',
href: '/compliance_alerts/compliance_alerts-list', href: '/vendors/vendors-list',
}, },
], ],
watchlistCards: [ watchlistCards: [
{ {
key: 'procurement-queue', key: 'procurement-queue',
title: 'Priority procurement queue', title: 'Priority sourcing queue',
collectionKey: 'procurementQueue', collectionKey: 'procurementQueue',
note: 'Newest or most urgent requisitions requiring action from the procurement desk.', note: 'Requisitions most likely to need procurement action next.',
href: '/requisitions/requisitions-list', href: '/requisitions/requisitions-list',
}, },
{ {
key: 'procurement-contract-watch', key: 'procurement-contract-watch',
title: 'Expiring contract watchlist', title: 'Contract renewal watchlist',
collectionKey: 'contractWatchlist', collectionKey: 'contractWatchlist',
note: 'Contracts most likely to create continuity or sourcing pressure soon.', note: 'Supplier commitments most likely to create continuity pressure soon.',
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
}, },
{ {
key: 'procurement-approval-watch', key: 'procurement-approval-watch',
title: 'Approval blockers to clear', title: 'Procurement approval watchlist',
collectionKey: 'approvalQueue', collectionKey: 'approvalQueue',
note: 'Pending approvals tied to the demand-to-award flow.', note: 'Pending approvals tied to the demand-to-award chain.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
], ],
@ -386,7 +386,7 @@ const workspacePayloadConfigs = {
key: 'compliance-contracts', key: 'compliance-contracts',
title: 'Contracts nearing obligation review', title: 'Contracts nearing obligation review',
metricKey: 'contractsNearingExpiry', metricKey: 'contractsNearingExpiry',
note: 'Expiring contracts that may require evidence review, amendments, or closure checks.', note: 'Expiring contracts that may require evidence review, amendment, or closure checks.',
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
}, },
{ {
@ -407,16 +407,16 @@ const workspacePayloadConfigs = {
}, },
{ {
key: 'compliance-approval-watch', key: 'compliance-approval-watch',
title: 'Approvals with control exposure', title: 'Approvals needing control review',
collectionKey: 'approvalQueue', collectionKey: 'approvalQueue',
note: 'Pending approvals where evidence, delegation, or routing should be verified.', note: 'Pending approvals where evidence, delegation, or routing should be verified.',
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
}, },
{ {
key: 'compliance-notification-watch', key: 'compliance-notification-watch',
title: 'Recent control notices', title: 'Control notice watchlist',
collectionKey: 'recentNotifications', collectionKey: 'recentNotifications',
note: 'Notifications that can help trace exceptions and follow-up points.', note: 'Recent notices that help trace exceptions and follow-up points.',
href: '/notifications/notifications-list', href: '/notifications/notifications-list',
}, },
], ],
@ -435,7 +435,7 @@ const workspacePayloadConfigs = {
key: 'delivery-progress', key: 'delivery-progress',
title: 'Average execution progress', title: 'Average execution progress',
metricKey: 'averageProjectProgress', metricKey: 'averageProjectProgress',
note: 'Average completion across approved and active projects in the accessible delivery scope.', note: 'Average completion across approved and active projects in the visible delivery scope.',
href: '/projects/projects-list', href: '/projects/projects-list',
}, },
{ {
@ -456,11 +456,18 @@ const workspacePayloadConfigs = {
watchlistCards: [ watchlistCards: [
{ {
key: 'delivery-rollout-watch', key: 'delivery-rollout-watch',
title: 'Province rollout snapshot', title: 'Province delivery watchlist',
collectionKey: 'provinceRollout', collectionKey: 'provinceRollout',
note: 'Provincial delivery concentration and completion spread across the current portfolio.', note: 'Provincial delivery concentration and completion spread across the current portfolio.',
href: '/projects/projects-list', href: '/projects/projects-list',
}, },
{
key: 'delivery-approval-watch',
title: 'Delivery blocker queue',
collectionKey: 'approvalQueue',
note: 'Pending approvals that can delay works, procurement, or milestone completion.',
href: '/approvals/approvals-list',
},
{ {
key: 'delivery-risk-watch', key: 'delivery-risk-watch',
title: 'Delivery risk watchlist', title: 'Delivery risk watchlist',
@ -468,13 +475,6 @@ const workspacePayloadConfigs = {
note: 'Open alerts most likely to disrupt project execution.', note: 'Open alerts most likely to disrupt project execution.',
href: '/compliance_alerts/compliance_alerts-list', href: '/compliance_alerts/compliance_alerts-list',
}, },
{
key: 'delivery-approval-watch',
title: 'Approvals affecting execution',
collectionKey: 'approvalQueue',
note: 'Pending approvals that can delay works, procurement, or milestone completion.',
href: '/approvals/approvals-list',
},
], ],
}, },
}; };

View File

@ -39,6 +39,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
(state) => state.style.activeLinkColor, (state) => state.style.activeLinkColor,
); );
const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : '' const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : ''
const isGroupTrigger = Boolean(item.menu && !item.href && !isDropdownList)
const { asPath, isReady } = useRouter() const { asPath, isReady } = useRouter()
@ -66,7 +67,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
<span <span
className={`grow text-ellipsis line-clamp-1 ${ className={`grow text-ellipsis line-clamp-1 ${
item.menu ? '' : 'pr-12' item.menu ? '' : 'pr-12'
} ${activeClassAddon}`} } ${isGroupTrigger ? 'text-[0.95rem] tracking-[0.01em]' : ''} ${activeClassAddon}`}
> >
{item.label} {item.label}
</span> </span>
@ -81,18 +82,19 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
) )
const componentClass = [ const componentClass = [
'flex cursor-pointer py-1.5 ', 'group flex items-center gap-1 rounded-xl border border-transparent px-3 transition-all duration-150',
isDropdownList ? 'px-6 text-sm' : '', isDropdownList ? 'py-2 text-sm' : 'py-2.5',
item.color item.color
? getButtonColor(item.color, false, true) ? getButtonColor(item.color, false, true)
: `${asideMenuItemStyle}`, : `${asideMenuItemStyle}`,
isGroupTrigger ? 'font-semibold' : '',
isLinkActive isLinkActive
? `text-black ${activeLinkColor} dark:text-white dark:bg-dark-800` ? `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(' '); ].join(' ');
return ( return (
<li className={'px-3 py-1.5'}> <li className={isDropdownList ? 'px-3 py-1' : 'px-3 py-1.5'}>
{item.withDevider && <hr className={`${borders} mb-3`} />} {item.withDevider && <hr className={`${borders} mb-3`} />}
{item.href && ( {item.href && (
<Link href={item.href} target={item.target} className={componentClass}> <Link href={item.href} target={item.target} className={componentClass}>
@ -108,7 +110,9 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
<AsideMenuList <AsideMenuList
menu={item.menu} menu={item.menu}
className={`${asideMenuDropdownStyle} ${ className={`${asideMenuDropdownStyle} ${
isDropdownActive ? 'block dark:bg-slate-800/50' : 'hidden' isDropdownActive
? 'mt-2 ml-3 block rounded-xl border border-slate-200/70 pl-2 dark:border-slate-800 dark:bg-slate-900/40'
: 'hidden'
}`} }`}
isDropdownList isDropdownList
/> />

View File

@ -89,6 +89,11 @@ export interface WorkspaceSectionCopy {
actionLabel?: string; actionLabel?: string;
} }
export interface WorkspaceBriefingCard {
title: string;
items: string[];
}
export interface WorkspaceConfig { export interface WorkspaceConfig {
sidebarLabel: string; sidebarLabel: string;
pageTitle: string; pageTitle: string;
@ -97,6 +102,7 @@ export interface WorkspaceConfig {
heroDescription: string; heroDescription: string;
primaryAction: WorkspaceAction; primaryAction: WorkspaceAction;
secondaryAction: WorkspaceAction; secondaryAction: WorkspaceAction;
briefingCards: WorkspaceBriefingCard[];
highlightedMetricKeys: WorkspaceMetricKey[]; highlightedMetricKeys: WorkspaceMetricKey[];
heroMetricKeys: WorkspaceMetricKey[]; heroMetricKeys: WorkspaceMetricKey[];
blockOrder: WorkspaceDetailBlockKey[]; blockOrder: WorkspaceDetailBlockKey[];
@ -104,11 +110,12 @@ export interface WorkspaceConfig {
quickLinks: WorkspaceQuickLink[]; quickLinks: WorkspaceQuickLink[];
} }
type WorkspaceConfigInput = Omit<WorkspaceConfig, 'sectionCopy' | 'heroMetricKeys' | 'blockOrder' | 'quickLinks'> & { type WorkspaceConfigInput = Omit<WorkspaceConfig, 'sectionCopy' | 'heroMetricKeys' | 'blockOrder' | 'quickLinks' | 'briefingCards'> & {
heroMetricKeys?: WorkspaceMetricKey[]; heroMetricKeys?: WorkspaceMetricKey[];
blockOrder?: WorkspaceDetailBlockKey[]; blockOrder?: WorkspaceDetailBlockKey[];
sectionCopy?: Partial<Record<WorkspaceSectionKey, WorkspaceSectionCopy>>; sectionCopy?: Partial<Record<WorkspaceSectionKey, WorkspaceSectionCopy>>;
quickLinks?: WorkspaceQuickLink[]; quickLinks?: WorkspaceQuickLink[];
briefingCards?: WorkspaceBriefingCard[];
}; };
const defaultBlockOrder: WorkspaceDetailBlockKey[] = [ const defaultBlockOrder: WorkspaceDetailBlockKey[] = [
@ -194,6 +201,7 @@ const createWorkspaceConfig = ({
blockOrder, blockOrder,
sectionCopy, sectionCopy,
quickLinks, quickLinks,
briefingCards,
...config ...config
}: WorkspaceConfigInput): WorkspaceConfig => ({ }: WorkspaceConfigInput): WorkspaceConfig => ({
...config, ...config,
@ -204,224 +212,291 @@ const createWorkspaceConfig = ({
...sectionCopy, ...sectionCopy,
}, },
quickLinks: quickLinks || defaultQuickLinks, quickLinks: quickLinks || defaultQuickLinks,
briefingCards: briefingCards || [],
}); });
const workspaceConfigs: Record<string, WorkspaceConfig> = { const workspaceConfigs: Record<string, WorkspaceConfig> = {
[WORKSPACE_ROLES.superAdmin]: createWorkspaceConfig({ [WORKSPACE_ROLES.superAdmin]: createWorkspaceConfig({
sidebarLabel: 'Platform Command', sidebarLabel: 'Platform Administration',
pageTitle: 'Platform Command Workspace', pageTitle: 'Platform Administration',
eyebrow: 'FDSU ERP · Tenant governance workspace', eyebrow: 'FDSU ERP · Platform Administration',
heroTitle: 'Govern tenants, access policy, institutional activity, and platform-level risk from one command surface.', heroTitle: 'Set the platform structure, govern access, and watch cross-organization control risk.',
heroDescription: heroDescription:
'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.', 'This workspace is for the Super Admin: organizations, top-level users, roles, permissions, and audit visibility. It keeps the platform safe and usable for every organization without turning the Super Admin into a day-to-day operator.',
primaryAction: { href: '/organizations/organizations-list', label: 'Review tenant registry' }, primaryAction: { href: '/organizations/organizations-list', label: 'Review organizations' },
secondaryAction: { href: '/users/users-list', label: 'Open access control' }, secondaryAction: { href: '/roles/roles-list', label: 'Review roles' },
highlightedMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'contractsNearingExpiry', 'unreadNotifications', 'vendorComplianceAlerts'], briefingCards: [
heroMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'unreadNotifications'], {
blockOrder: ['focus', 'watchlist', 'summary', 'approvalRisk', 'operations', 'delivery', 'actions'], title: 'Should see',
items: ['Organizations and tenant coverage', 'Platform users, roles, and permissions', 'Audit activity and cross-organization access exceptions'],
},
{
title: 'Should do',
items: ['Set platform governance and permission boundaries', 'Resolve top-level access issues', 'Support administrators without running daily transactions'],
},
{
title: 'Receives from',
items: ['Tenant setup requests', 'Access-governance escalations', 'Platform-wide audit and control signals'],
},
{
title: 'Hands off to',
items: ['Administrators for organization execution', 'Functional leads for business decisions', 'Tenant owners for local follow-through'],
},
],
highlightedMetricKeys: ['pendingApprovals', 'openRiskAlerts', 'unreadNotifications', 'vendorComplianceAlerts', 'activeProjects', 'contractsNearingExpiry'],
heroMetricKeys: ['pendingApprovals', 'openRiskAlerts', 'unreadNotifications', 'vendorComplianceAlerts'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Governance queue', eyebrow: 'Governance queue',
title: 'Escalations and approvals requiring governance attention', title: 'Escalations and approvals that need platform-level attention',
actionLabel: 'Open governance queue', actionLabel: 'Open governance queue',
}, },
riskPanel: { riskPanel: {
eyebrow: 'Cross-cutting control flags', eyebrow: 'Platform control exposure',
title: 'Platform-wide exceptions, permission exposure, and institutional control risk', title: 'Cross-organization exceptions, access issues, and audit signals',
}, },
recentNotifications: { recentNotifications: {
eyebrow: 'Cross-tenant activity', eyebrow: 'Cross-organization notices',
title: 'Recent workflow and control signals across organizations', title: 'Recent notices that can reveal access, setup, or control problems',
actionLabel: 'Open activity feed', actionLabel: 'Open notices',
}, },
quickActions: { quickActions: {
eyebrow: 'Platform actions', eyebrow: 'Platform administration shortcuts',
title: 'Jump directly into tenant oversight and permission-control work', title: 'Go directly to organizations, users, roles, and permissions',
}, },
}, },
quickLinks: [ quickLinks: [
{ {
href: '/organizations/organizations-list', href: '/organizations/organizations-list',
label: 'Organization registry', label: 'Organizations',
description: 'Review tenant setup, coverage, and stewardship ownership.', description: 'Review tenant setup, ownership, and coverage.',
icon: 'organizations', icon: 'organizations',
}, },
{ {
href: '/users/users-list', href: '/users/users-list',
label: 'User access', label: 'Users',
description: 'Manage accounts, authority assignments, and platform ownership.', description: 'Manage top-level accounts and platform access.',
icon: 'users', icon: 'users',
}, },
{ {
href: '/roles/roles-list', href: '/roles/roles-list',
label: 'Role catalog', label: 'Roles',
description: 'Govern platform-wide role definitions and responsibility boundaries.', description: 'Define responsibility boundaries across the platform.',
icon: 'users', icon: 'users',
}, },
{ {
href: '/permissions/permissions-list', href: '/permissions/permissions-list',
label: 'Permission matrix', label: 'Permissions',
description: 'Inspect and tighten permission coverage across the platform.', description: 'Inspect and tighten the permission matrix.',
icon: 'audit', icon: 'audit',
}, },
], ],
}), }),
[WORKSPACE_ROLES.administrator]: createWorkspaceConfig({ [WORKSPACE_ROLES.administrator]: createWorkspaceConfig({
sidebarLabel: 'Operations Command', sidebarLabel: 'Organization Administration',
pageTitle: 'Operations Command Workspace', pageTitle: 'Organization Administration',
eyebrow: 'FDSU ERP · Workflow operations workspace', eyebrow: 'FDSU ERP · Organization Administration',
heroTitle: 'Run workflow operations, user support, record readiness, and day-to-day administrative follow-through.', heroTitle: 'Keep the organization ready to operate: users, routing, notices, and master data.',
heroDescription: heroDescription:
'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.', 'This workspace is for the Administrator. It owns user setup, departments, provinces, approval routing, notifications, and day-to-day system readiness. It should not read like a finance, procurement, or project command center.',
primaryAction: { href: '/approval_workflows/approval_workflows-list', label: 'Open workflow hub' }, primaryAction: { href: '/approval_workflows/approval_workflows-list', label: 'Review workflow setup' },
secondaryAction: { href: '/notifications/notifications-list', label: 'Open notice center' }, secondaryAction: { href: '/notifications/notifications-list', label: 'Review notifications' },
highlightedMetricKeys: ['pendingApprovals', 'unreadNotifications', 'contractsNearingExpiry', 'openRiskAlerts', 'activeProjects', 'procurementPipeline'], briefingCards: [
heroMetricKeys: ['pendingApprovals', 'unreadNotifications', 'procurementPipeline', 'contractsNearingExpiry'], {
blockOrder: ['focus', 'summary', 'approvalRisk', 'watchlist', 'operations', 'delivery', 'actions'], title: 'Should see',
items: ['Workflow readiness and routing issues', 'Users, departments, provinces, and notices', 'Configuration gaps slowing daily operations'],
},
{
title: 'Should do',
items: ['Onboard and support users', 'Maintain approval steps, delegations, and notifications', 'Keep administrative records clean and usable'],
},
{
title: 'Receives from',
items: ['Super Admin policy and access structure', 'Support requests from staff', 'Broken-routing or setup escalations from functional leads'],
},
{
title: 'Hands off to',
items: ['Operational staff for daily execution', 'Functional leads for domain decisions', 'Director General when administrative blockages affect the institution'],
},
],
highlightedMetricKeys: ['pendingApprovals', 'unreadNotifications', 'openRiskAlerts', 'vendorComplianceAlerts'],
heroMetricKeys: ['pendingApprovals', 'unreadNotifications', 'openRiskAlerts', 'vendorComplianceAlerts'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Operations queue', eyebrow: 'Workflow routing queue',
title: 'Approvals waiting on routing, delegation, or admin follow-through', title: 'Approvals waiting because routing, delegation, or setup needs attention',
actionLabel: 'Open workflow queue', actionLabel: 'Open approval queue',
}, },
procurementQueue: { riskPanel: {
eyebrow: 'Operational throughput', eyebrow: 'Administrative control signals',
title: 'Requisitions that may require workflow or setup unblock', title: 'Alerts and exceptions that often point to setup, delegation, or record issues',
actionLabel: 'Open requisitions',
}, },
recentNotifications: { recentNotifications: {
eyebrow: 'Operational notices', eyebrow: 'Administrator notices',
title: 'Recent admin-facing signals requiring follow-through', title: 'Support and system notices requiring administrator follow-through',
actionLabel: 'Open notice center', actionLabel: 'Open notices',
}, },
quickActions: { quickActions: {
eyebrow: 'Administrative actions', eyebrow: 'Administration shortcuts',
title: 'Open grouped control areas without scanning the full ERP register', title: 'Go straight to users, workflow setup, notifications, and organization records',
}, },
}, },
quickLinks: [ quickLinks: [
{
href: '/users/users-list',
label: 'Users',
description: 'Manage onboarding, account support, and assignments.',
icon: 'users',
},
{ {
href: '/approval_workflows/approval_workflows-list', href: '/approval_workflows/approval_workflows-list',
label: 'Workflow hub', label: 'Approval workflows',
description: 'Keep approval routes, steps, and queues ready for daily use.', description: 'Maintain routes, steps, and approval readiness.',
icon: 'approvals', icon: 'approvals',
}, },
{ {
href: '/notifications/notifications-list', href: '/notifications/notifications-list',
label: 'Notice center', label: 'Notifications',
description: 'Triage notifications, documents, and control signals in one stop.', description: 'Review notices, workflow signals, and support follow-up.',
icon: 'notifications', icon: 'notifications',
}, },
{ {
href: '/users/users-list', href: '/departments/departments-list',
label: 'Admin records', label: 'Departments',
description: 'Jump into users, departments, provinces, and support clean-up.', description: 'Keep organization structure and master data aligned.',
icon: 'users', icon: 'users',
}, },
{
href: '/projects/projects-list',
label: 'Delivery register',
description: 'Move from grouped navigation into active projects and contract follow-through.',
icon: 'projects',
},
], ],
}), }),
[WORKSPACE_ROLES.directorGeneral]: createWorkspaceConfig({ [WORKSPACE_ROLES.directorGeneral]: createWorkspaceConfig({
sidebarLabel: 'Executive Command', sidebarLabel: 'Executive Oversight',
pageTitle: 'Director General Workspace', pageTitle: 'Executive Oversight',
eyebrow: 'FDSU ERP · Director General workspace', eyebrow: 'FDSU ERP · Executive Oversight',
heroTitle: 'Track strategic budget use, delivery momentum, risk concentration, and contract exposure across the institution.', heroTitle: 'See the institution across finance, procurement, compliance, and delivery, then make executive decisions.',
heroDescription: heroDescription:
'This workspace emphasizes executive oversight over budgets, projects, approvals, and risk hot spots while staying connected to the underlying contracts, requisitions, and implementation records.', 'This workspace is for the Director General. It brings together budget posture, delivery progress, risk concentration, and high-impact approvals so executive attention stays on direction, escalation, and accountability.',
primaryAction: { href: '/projects/projects-list', label: 'Review projects' }, primaryAction: { href: '/approvals/approvals-list', label: 'Review escalations' },
secondaryAction: { href: '/contracts/contracts-list', label: 'Review contracts' }, secondaryAction: { href: '/projects/projects-list', label: 'Review project portfolio' },
briefingCards: [
{
title: 'Should see',
items: ['Strategic budget posture', 'Delivery momentum and risk concentration', 'Major approvals and contracts needing executive direction'],
},
{
title: 'Should do',
items: ['Approve high-impact decisions', 'Set priorities across functions', 'Resolve escalations that cross departmental boundaries'],
},
{
title: 'Receives from',
items: ['Finance, procurement, compliance, and delivery leads', 'Administrator escalations with institution-wide impact'],
},
{
title: 'Hands off to',
items: ['Functional leads for execution', 'Administrator for coordination follow-through', 'Operational teams through approved decisions'],
},
],
highlightedMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects', 'contractsNearingExpiry'], highlightedMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects', 'contractsNearingExpiry'],
heroMetricKeys: ['approvedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects'], heroMetricKeys: ['approvedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects'],
blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'operations', 'actions'], blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'actions'],
sectionCopy: { sectionCopy: {
riskPanel: { riskPanel: {
eyebrow: 'Executive risk watch', eyebrow: 'Executive risk watch',
title: 'Strategic risks, bottlenecks, and control exposure', title: 'Strategic risks, bottlenecks, and control exposure needing executive attention',
}, },
provinceRollout: { provinceRollout: {
eyebrow: 'Delivery coverage', eyebrow: 'Delivery coverage',
title: 'Provincial rollout and execution pace', title: 'Project rollout and execution pace across provinces',
actionLabel: 'Open projects', actionLabel: 'Open projects',
}, },
topContracts: { topContracts: {
eyebrow: 'Contract exposure', eyebrow: 'Major commitments',
title: 'Largest commitments shaping institutional delivery', title: 'Largest contracts shaping institutional exposure and delivery',
actionLabel: 'Vendor and contract view', actionLabel: 'Open contracts',
}, },
quickActions: { quickActions: {
eyebrow: 'Executive actions', eyebrow: 'Executive shortcuts',
title: 'Jump into the records most relevant for strategic follow-up', title: 'Open the records most relevant to executive review and escalation',
}, },
}, },
quickLinks: [ quickLinks: [
{
href: '/approvals/approvals-list',
label: 'Escalations',
description: 'Review approvals and issues that need executive decision.',
icon: 'approvals',
},
{ {
href: '/projects/projects-list', href: '/projects/projects-list',
label: 'Project portfolio', label: 'Projects',
description: 'Review delivery status and ownership.', description: 'Review delivery status and accountability.',
icon: 'projects', icon: 'projects',
}, },
{ {
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
label: 'Contract exposure', label: 'Contracts',
description: 'Inspect major commitments and expiry dates.', description: 'Inspect major commitments and expiry exposure.',
icon: 'contracts', icon: 'contracts',
}, },
{
href: '/approvals/approvals-list',
label: 'Decision queue',
description: 'Track items waiting for institutional action.',
icon: 'approvals',
},
{ {
href: '/notifications/notifications-list', href: '/notifications/notifications-list',
label: 'Executive signals', label: 'Executive notices',
description: 'Review the latest alerts and escalations.', description: 'See the latest escalations and control signals.',
icon: 'notifications', icon: 'notifications',
}, },
], ],
}), }),
[WORKSPACE_ROLES.financeDirector]: createWorkspaceConfig({ [WORKSPACE_ROLES.financeDirector]: createWorkspaceConfig({
sidebarLabel: 'Financial Control', sidebarLabel: 'Financial Management',
pageTitle: 'Finance Director Workspace', pageTitle: 'Financial Management',
eyebrow: 'FDSU ERP · Finance Director workspace', eyebrow: 'FDSU ERP · Financial Management',
heroTitle: 'Monitor allocations, commitments, invoices, payments, and approval pressure with finance-first operational context.', heroTitle: 'Control budget, commitments, disbursements, and payment pressure.',
heroDescription: heroDescription:
'This workspace focuses on fiscal control across allocations, commitments, disbursements, overdue payment requests, and approvals while remaining linked to the procurement and project records driving spend.', 'This workspace is for the Finance Director. It focuses on allocations, commitments, disbursements, aging payment requests, and finance approvals while staying tied to the contracts and spending decisions that drive exposure.',
primaryAction: { href: '/payment_requests/payment_requests-list', label: 'Review payment requests' }, primaryAction: { href: '/payment_requests/payment_requests-list', label: 'Review payment requests' },
secondaryAction: { href: '/allocations/allocations-list', label: 'Review allocations' }, secondaryAction: { href: '/allocations/allocations-list', label: 'Review allocations' },
briefingCards: [
{
title: 'Should see',
items: ['Approved versus committed budget', 'Payment backlog and disbursement risk', 'Allocation pressure and finance approvals'],
},
{
title: 'Should do',
items: ['Validate funding coverage', 'Clear finance approvals and payment bottlenecks', 'Escalate shortfalls and fiscal-control issues'],
},
{
title: 'Receives from',
items: ['Operational finance staff', 'Procurement commitments and award decisions', 'Administrator-routed approvals'],
},
{
title: 'Hands off to',
items: ['Director General for major funding decisions', 'Payment and ledger staff for execution', 'Procurement and delivery leads on affordability constraints'],
},
],
highlightedMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments', 'pendingApprovals', 'unreadNotifications'], highlightedMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments', 'pendingApprovals', 'unreadNotifications'],
heroMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments'], heroMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments'],
blockOrder: ['summary', 'watchlist', 'focus', 'approvalRisk', 'delivery', 'operations', 'actions'], blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Finance queue', eyebrow: 'Finance approval queue',
title: 'Approvals affecting commitments and disbursements', title: 'Approvals affecting commitments, disbursements, and payment timing',
actionLabel: 'Open finance approvals', actionLabel: 'Open finance approvals',
}, },
riskPanel: { riskPanel: {
eyebrow: 'Fiscal control panel', eyebrow: 'Fiscal exposure',
title: 'Budget pressure, overdue payments, and control exceptions', title: 'Budget pressure, aging payments, and control exceptions affecting finance',
},
topContracts: {
eyebrow: 'High-value commitments',
title: 'Largest contracts influencing current financial exposure',
actionLabel: 'Open vendor master',
}, },
quickActions: { quickActions: {
eyebrow: 'Finance actions', eyebrow: 'Finance shortcuts',
title: 'Move from budget control into payment and allocation follow-up', title: 'Go directly to allocations, payment requests, commitments, and approvals',
}, },
}, },
quickLinks: [ quickLinks: [
{ {
href: '/payment_requests/payment_requests-list', href: '/payment_requests/payment_requests-list',
label: 'Payment requests', label: 'Payment requests',
description: 'Review overdue and pending disbursements.', description: 'Review pending and aging disbursement requests.',
icon: 'payments', icon: 'payments',
}, },
{ {
@ -432,108 +507,144 @@ const workspaceConfigs: Record<string, WorkspaceConfig> = {
}, },
{ {
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
label: 'Commitment register', label: 'Commitments',
description: 'Inspect the contracts driving spend.', description: 'Inspect the contracts driving current spend exposure.',
icon: 'contracts', icon: 'contracts',
}, },
{ {
href: '/approvals/approvals-list', href: '/approvals/approvals-list',
label: 'Approval pressure', label: 'Finance approvals',
description: 'Track bottlenecks holding back finance action.', description: 'Track bottlenecks holding back finance action.',
icon: 'approvals', icon: 'approvals',
}, },
], ],
}), }),
[WORKSPACE_ROLES.procurementLead]: createWorkspaceConfig({ [WORKSPACE_ROLES.procurementLead]: createWorkspaceConfig({
sidebarLabel: 'Procurement Desk', sidebarLabel: 'Procurement Management',
pageTitle: 'Procurement Lead Workspace', pageTitle: 'Procurement Management',
eyebrow: 'FDSU ERP · Procurement Lead workspace', eyebrow: 'FDSU ERP · Procurement Management',
heroTitle: 'Stay on top of requisitions, tender flow, contract deadlines, and approval backlog across the procurement chain.', heroTitle: 'Own the demand-to-award chain: requisitions, tenders, vendors, awards, and contract readiness.',
heroDescription: heroDescription:
'This workspace keeps procurement leadership focused on live demand, tender progression, expiring contracts, and workflow bottlenecks while preserving drill-down into vendors, awards, and originating requisitions.', 'This workspace is for the Procurement Lead. It keeps sourcing leadership focused on requisition inflow, tender progression, vendor readiness, contract deadlines, and approvals that are slowing the procurement chain.',
primaryAction: { href: '/requisitions/requisitions-list', label: 'Review requisitions' }, primaryAction: { href: '/requisitions/requisitions-list', label: 'Review requisitions' },
secondaryAction: { href: '/tenders/tenders-list', label: 'Open tender pipeline' }, secondaryAction: { href: '/tenders/tenders-list', label: 'Review tenders' },
briefingCards: [
{
title: 'Should see',
items: ['Requisition inflow and tender progress', 'Expiring contracts and supplier readiness', 'Approvals slowing sourcing and award decisions'],
},
{
title: 'Should do',
items: ['Move demand into sourcing', 'Review awards and vendor readiness', 'Escalate blockers in the procurement chain'],
},
{
title: 'Receives from',
items: ['Operational requesters and departments', 'Project teams needing sourcing support', 'Administrator workflow routing and setup support'],
},
{
title: 'Hands off to',
items: ['Finance on commitments and funding coverage', 'Compliance on control concerns', 'Director General on high-value or escalated decisions'],
},
],
highlightedMetricKeys: ['procurementPipeline', 'pendingApprovals', 'contractsNearingExpiry', 'vendorComplianceAlerts', 'openRiskAlerts', 'activeProjects'], highlightedMetricKeys: ['procurementPipeline', 'pendingApprovals', 'contractsNearingExpiry', 'vendorComplianceAlerts', 'openRiskAlerts', 'activeProjects'],
heroMetricKeys: ['procurementPipeline', 'pendingApprovals', 'contractsNearingExpiry', 'vendorComplianceAlerts'], heroMetricKeys: ['procurementPipeline', 'pendingApprovals', 'contractsNearingExpiry', 'vendorComplianceAlerts'],
blockOrder: ['focus', 'summary', 'operations', 'watchlist', 'approvalRisk', 'delivery', 'actions'], blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'operations', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Procurement approvals', eyebrow: 'Procurement approval queue',
title: 'Decisions blocking sourcing and award progress', title: 'Decisions blocking sourcing, evaluation, award, or contract signature',
actionLabel: 'Open approval backlog', actionLabel: 'Open approval backlog',
}, },
procurementQueue: { procurementQueue: {
eyebrow: 'Sourcing pipeline', eyebrow: 'Sourcing pipeline',
title: 'Requisitions and tenders requiring procurement action', title: 'Requisitions and tenders currently needing procurement action',
actionLabel: 'Open sourcing queue', actionLabel: 'Open sourcing queue',
}, },
contractWatchlist: { contractWatchlist: {
eyebrow: 'Contract renewals', eyebrow: 'Contract renewals',
title: 'Supplier commitments nearing expiry or follow-up', title: 'Supplier commitments nearing expiry or needing follow-through',
actionLabel: 'Open contracts', actionLabel: 'Open contracts',
}, },
quickActions: { quickActions: {
eyebrow: 'Procurement actions', eyebrow: 'Procurement shortcuts',
title: 'Jump into the next sourcing and contract-management tasks', title: 'Go directly to requisitions, tenders, vendors, and contract follow-up',
}, },
}, },
quickLinks: [ quickLinks: [
{ {
href: '/requisitions/requisitions-list', href: '/requisitions/requisitions-list',
label: 'Requisition queue', label: 'Requisitions',
description: 'Review demand and readiness to source.', description: 'Review new demand and sourcing readiness.',
icon: 'requisitions', icon: 'requisitions',
}, },
{ {
href: '/tenders/tenders-list', href: '/tenders/tenders-list',
label: 'Tender pipeline', label: 'Tenders',
description: 'Follow active tenders and consultations.', description: 'Follow active tenders and consultations.',
icon: 'tenders', icon: 'tenders',
}, },
{ {
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
label: 'Contract deadlines', label: 'Contracts',
description: 'Track expiring commitments and supplier action.', description: 'Track expiring commitments and follow-up actions.',
icon: 'contracts', icon: 'contracts',
}, },
{ {
href: '/vendors/vendors-list', href: '/vendors/vendors-list',
label: 'Vendor register', label: 'Vendors',
description: 'Inspect supplier readiness and history.', description: 'Inspect supplier readiness and history.',
icon: 'vendors', icon: 'vendors',
}, },
], ],
}), }),
[WORKSPACE_ROLES.complianceAuditLead]: createWorkspaceConfig({ [WORKSPACE_ROLES.complianceAuditLead]: createWorkspaceConfig({
sidebarLabel: 'Compliance Desk', sidebarLabel: 'Compliance & Audit',
pageTitle: 'Compliance & Audit Workspace', pageTitle: 'Compliance & Audit',
eyebrow: 'FDSU ERP · Compliance & Audit workspace', eyebrow: 'FDSU ERP · Compliance & Audit',
heroTitle: 'Surface red flags, document gaps, control exceptions, and approval exposure before they become institutional failures.', heroTitle: 'Watch control breaches, evidence gaps, expiring obligations, and approvals that need independent review.',
heroDescription: heroDescription:
'This workspace is optimized for compliance and audit follow-up: open alerts, expiring obligations, evidence gaps, approvals awaiting control review, and links into the exact underlying ERP records.', 'This workspace is for the Compliance and Audit Lead. It focuses on red flags, evidence quality, audit trails, expiring obligations, and approvals where controls must be checked before risk becomes failure.',
primaryAction: { href: '/compliance_alerts/compliance_alerts-list', label: 'Review compliance alerts' }, primaryAction: { href: '/compliance_alerts/compliance_alerts-list', label: 'Review compliance alerts' },
secondaryAction: { href: '/audit_logs/audit_logs-list', label: 'Open audit logs' }, secondaryAction: { href: '/audit_logs/audit_logs-list', label: 'Review audit logs' },
briefingCards: [
{
title: 'Should see',
items: ['Compliance alerts and evidence gaps', 'Expiring obligations and aging items', 'Approvals requiring control review'],
},
{
title: 'Should do',
items: ['Investigate red flags', 'Trace actions in audit logs', 'Push corrective action and evidence follow-up'],
},
{
title: 'Receives from',
items: ['System alerts and audit trails', 'Functional leads reporting control exceptions', 'Administrator notices and escalations'],
},
{
title: 'Hands off to',
items: ['Director General on serious exposure', 'Process owners for remediation', 'Staff for evidence completion and follow-up'],
},
],
highlightedMetricKeys: ['openRiskAlerts', 'vendorComplianceAlerts', 'contractsNearingExpiry', 'pendingApprovals', 'overduePayments', 'unreadNotifications'], highlightedMetricKeys: ['openRiskAlerts', 'vendorComplianceAlerts', 'contractsNearingExpiry', 'pendingApprovals', 'overduePayments', 'unreadNotifications'],
heroMetricKeys: ['openRiskAlerts', 'vendorComplianceAlerts', 'contractsNearingExpiry', 'pendingApprovals'], heroMetricKeys: ['openRiskAlerts', 'vendorComplianceAlerts', 'contractsNearingExpiry', 'pendingApprovals'],
blockOrder: ['watchlist', 'focus', 'summary', 'approvalRisk', 'operations', 'delivery', 'actions'], blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Control review queue', eyebrow: 'Control review queue',
title: 'Approvals and cases requiring audit follow-through', title: 'Approvals and cases requiring audit or compliance follow-through',
actionLabel: 'Open control queue', actionLabel: 'Open control queue',
}, },
riskPanel: { riskPanel: {
eyebrow: 'Compliance watch', eyebrow: 'Compliance watch',
title: 'Exceptions, overdue items, and evidence gaps', title: 'Exceptions, evidence gaps, overdue items, and red flags needing review',
}, },
recentNotifications: { recentNotifications: {
eyebrow: 'Alert traffic', eyebrow: 'Control notices',
title: 'Recent system signals and policy-relevant activity', title: 'Recent signals that may indicate policy, evidence, or routing problems',
actionLabel: 'Open alert feed', actionLabel: 'Open notices',
}, },
quickActions: { quickActions: {
eyebrow: 'Audit actions', eyebrow: 'Compliance shortcuts',
title: 'Go straight to alerts, logs, and records needing review', title: 'Go directly to alerts, logs, approvals, and obligations needing review',
}, },
}, },
quickLinks: [ quickLinks: [
@ -557,49 +668,67 @@ const workspaceConfigs: Record<string, WorkspaceConfig> = {
}, },
{ {
href: '/contracts/contracts-list', href: '/contracts/contracts-list',
label: 'Contract obligations', label: 'Obligations',
description: 'Check commitments nearing expiry or breach.', description: 'Check commitments nearing expiry or breach.',
icon: 'contracts', icon: 'contracts',
}, },
], ],
}), }),
[WORKSPACE_ROLES.projectDeliveryLead]: createWorkspaceConfig({ [WORKSPACE_ROLES.projectDeliveryLead]: createWorkspaceConfig({
sidebarLabel: 'Delivery Command', sidebarLabel: 'Project Delivery',
pageTitle: 'Project Delivery Workspace', pageTitle: 'Project Delivery',
eyebrow: 'FDSU ERP · Project Delivery workspace', eyebrow: 'FDSU ERP · Project Delivery',
heroTitle: 'Keep execution moving by watching project progress, blocked approvals, payment lag, and field-level delivery risk.', heroTitle: 'Keep projects moving by watching milestones, execution risk, approvals, and payment delays.',
heroDescription: heroDescription:
'This workspace centers on implementation performance: active projects, milestone progress, approvals affecting execution, and risks that can delay delivery across provinces and programs.', 'This workspace is for the Project Delivery Lead. It centers on implementation progress, field issues, delivery blockers, and the finance or procurement dependencies that can delay execution.',
primaryAction: { href: '/projects/projects-list', label: 'Review projects' }, primaryAction: { href: '/projects/projects-list', label: 'Review projects' },
secondaryAction: { href: '/project_milestones/project_milestones-list', label: 'Review milestones' }, secondaryAction: { href: '/project_milestones/project_milestones-list', label: 'Review milestones' },
briefingCards: [
{
title: 'Should see',
items: ['Project status and milestone slippage', 'Field issues and execution risks', 'Approvals or payments delaying delivery'],
},
{
title: 'Should do',
items: ['Review project progress', 'Clear delivery blockers with other leads', 'Escalate risks before timelines fail'],
},
{
title: 'Receives from',
items: ['Project staff and field verification teams', 'Procurement and finance status updates', 'Administrator workflow escalations'],
},
{
title: 'Hands off to',
items: ['Director General for major decisions', 'Procurement and finance leads on blockers', 'Operational project teams for execution'],
},
],
highlightedMetricKeys: ['activeProjects', 'averageProjectProgress', 'highRiskProjects', 'pendingApprovals', 'overduePayments', 'unreadNotifications'], highlightedMetricKeys: ['activeProjects', 'averageProjectProgress', 'highRiskProjects', 'pendingApprovals', 'overduePayments', 'unreadNotifications'],
heroMetricKeys: ['activeProjects', 'averageProjectProgress', 'highRiskProjects', 'pendingApprovals'], heroMetricKeys: ['activeProjects', 'averageProjectProgress', 'highRiskProjects', 'pendingApprovals'],
blockOrder: ['summary', 'focus', 'delivery', 'approvalRisk', 'watchlist', 'operations', 'actions'], blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'actions'],
sectionCopy: { sectionCopy: {
approvalQueue: { approvalQueue: {
eyebrow: 'Execution blockers', eyebrow: 'Delivery blocker queue',
title: 'Approvals holding back project delivery', title: 'Approvals holding back project execution',
actionLabel: 'Open delivery blockers', actionLabel: 'Open delivery blockers',
}, },
provinceRollout: { provinceRollout: {
eyebrow: 'Delivery heatmap', eyebrow: 'Delivery footprint',
title: 'Progress by province and execution footprint', title: 'Project progress and execution pace by province',
actionLabel: 'Open project register', actionLabel: 'Open project register',
}, },
topContracts: { topContracts: {
eyebrow: 'Major delivery contracts', eyebrow: 'Delivery contracts',
title: 'Largest commitments underpinning project execution', title: 'Largest contracts underpinning execution and milestone delivery',
actionLabel: 'Open suppliers', actionLabel: 'Open contracts',
}, },
quickActions: { quickActions: {
eyebrow: 'Delivery actions', eyebrow: 'Delivery shortcuts',
title: 'Move into the projects, milestones, and blockers that need attention', title: 'Go directly to projects, milestones, blockers, and supporting contracts',
}, },
}, },
quickLinks: [ quickLinks: [
{ {
href: '/projects/projects-list', href: '/projects/projects-list',
label: 'Project portfolio', label: 'Projects',
description: 'Review implementation status and ownership.', description: 'Review implementation status and ownership.',
icon: 'projects', icon: 'projects',
}, },

View File

@ -706,13 +706,13 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiBankOutline, icon: icon.mdiBankOutline,
label: 'Role Workspace', label: 'Role Workspace',
labelByRole: { labelByRole: {
[WORKSPACE_ROLES.superAdmin]: 'Platform Command', [WORKSPACE_ROLES.superAdmin]: 'Platform Administration',
[WORKSPACE_ROLES.administrator]: 'Operations Command', [WORKSPACE_ROLES.administrator]: 'Organization Administration',
[WORKSPACE_ROLES.directorGeneral]: 'Executive Command', [WORKSPACE_ROLES.directorGeneral]: 'Executive Oversight',
[WORKSPACE_ROLES.financeDirector]: 'Financial Control', [WORKSPACE_ROLES.financeDirector]: 'Financial Management',
[WORKSPACE_ROLES.procurementLead]: 'Procurement Desk', [WORKSPACE_ROLES.procurementLead]: 'Procurement Management',
[WORKSPACE_ROLES.complianceAuditLead]: 'Compliance Desk', [WORKSPACE_ROLES.complianceAuditLead]: 'Compliance & Audit',
[WORKSPACE_ROLES.projectDeliveryLead]: 'Delivery Command', [WORKSPACE_ROLES.projectDeliveryLead]: 'Project Delivery',
}, },
}, },
{ {

View File

@ -259,7 +259,7 @@ const statusBadgeClass = (status?: string) => {
case 'failed': case 'failed':
return 'bg-red-50 text-red-700 border-red-200'; return 'bg-red-50 text-red-700 border-red-200';
default: default:
return 'bg-slate-100 text-slate-700 border-slate-200'; return 'bg-slate-100 text-slate-700 dark:text-slate-300 border-slate-200';
} }
}; };
@ -272,7 +272,7 @@ const severityBadgeClass = (severity?: string) => {
case 'warning': case 'warning':
return 'bg-amber-50 text-amber-700 border-amber-200'; return 'bg-amber-50 text-amber-700 border-amber-200';
default: default:
return 'bg-slate-100 text-slate-700 border-slate-200'; return 'bg-slate-100 text-slate-700 dark:text-slate-300 border-slate-200';
} }
}; };
@ -310,10 +310,10 @@ const SectionHeader = ({
title: string; title: string;
action?: ReactNode; action?: ReactNode;
}) => ( }) => (
<div className='flex items-center justify-between gap-4 border-b border-slate-200 pb-4'> <div className='flex items-center justify-between gap-4 border-b border-slate-200 pb-4 dark:border-slate-800'>
<div> <div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>{eyebrow}</p> <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'>{title}</h3> <h3 className='mt-1 text-xl font-semibold text-slate-900 dark:text-white'>{title}</h3>
</div> </div>
{action} {action}
</div> </div>
@ -330,20 +330,88 @@ const SummaryMetric = ({
note: string; note: string;
icon: string; icon: string;
}) => ( }) => (
<CardBox className='h-full border border-slate-200 bg-white'> <CardBox className='h-full border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:shadow-none'>
<div className='flex items-start justify-between gap-3'> <div className='flex items-start justify-between gap-3'>
<div> <div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>{title}</p> <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'>{value}</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'>{note}</p> <p className='mt-2 text-sm text-slate-500 dark:text-slate-400'>{note}</p>
</div> </div>
<div className='flex h-11 w-11 items-center justify-center rounded-md border border-slate-200 bg-slate-50'> <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'>
<BaseIcon path={icon} size={22} className='text-slate-700' /> <BaseIcon path={icon} size={22} className='text-slate-700 dark:text-slate-200' />
</div> </div>
</div> </div>
</CardBox> </CardBox>
); );
const getBriefingCardStyle = (title: string) => {
const normalizedTitle = title.toLowerCase();
if (normalizedTitle.includes('should see')) {
return {
icon: mdiChartTimelineVariant,
badgeClass: 'border-blue-200 bg-blue-50 text-blue-700',
iconClass: 'border-blue-200 bg-blue-100 text-blue-700',
};
}
if (normalizedTitle.includes('should do')) {
return {
icon: mdiClipboardListOutline,
badgeClass: 'border-emerald-200 bg-emerald-50 text-emerald-700',
iconClass: 'border-emerald-200 bg-emerald-100 text-emerald-700',
};
}
if (normalizedTitle.includes('receive')) {
return {
icon: mdiBellOutline,
badgeClass: 'border-amber-200 bg-amber-50 text-amber-700',
iconClass: 'border-amber-200 bg-amber-100 text-amber-700',
};
}
return {
icon: mdiCheckDecagramOutline,
badgeClass: 'border-violet-200 bg-violet-50 text-violet-700',
iconClass: 'border-violet-200 bg-violet-100 text-violet-700',
};
};
const RoleBriefingCard = ({
title,
items,
}: {
title: string;
items: string[];
}) => {
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'>
<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}`}>
<BaseIcon path={style.icon} size={20} />
</div>
</div>
<ul className='space-y-2 text-sm leading-6 text-slate-600 dark:text-slate-300'>
{items.map((item) => (
<li key={`${title}-${item}`} className='flex items-start gap-2'>
<span className='mt-2 h-1.5 w-1.5 rounded-full bg-slate-400 dark:bg-slate-500' />
<span>{item}</span>
</li>
))}
</ul>
</div>
</CardBox>
);
};
const ExecutiveSummaryPage = () => { const ExecutiveSummaryPage = () => {
const router = useRouter(); const router = useRouter();
const { currentUser } = useAppSelector((state) => state.auth); const { currentUser } = useAppSelector((state) => state.auth);
@ -479,16 +547,20 @@ const ExecutiveSummaryPage = () => {
[metricDefinitions, workspaceConfig.heroMetricKeys], [metricDefinitions, workspaceConfig.heroMetricKeys],
); );
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 focusBlock = Boolean(data.workspace?.focusCards?.length) && ( const focusBlock = Boolean(data.workspace?.focusCards?.length) && (
<div className='mb-6 grid grid-cols-1 gap-4 xl:grid-cols-4'> <div className='mb-6 grid grid-cols-1 gap-4 xl:grid-cols-4'>
{data.workspace?.focusCards.map((card) => ( {data.workspace?.focusCards.map((card) => (
<CardBox key={card.key} className='border border-slate-200 bg-white'> <CardBox key={card.key} className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<div className='flex h-full flex-col justify-between gap-4'> <div className='flex h-full flex-col justify-between gap-4'>
<div> <div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>Role focus</p> <p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>Current priority</p>
<h3 className='mt-2 text-lg font-semibold text-slate-900'>{card.title}</h3> <h3 className='mt-2 text-lg font-semibold text-slate-900 dark:text-white'>{card.title}</h3>
<p className='mt-4 text-3xl font-semibold tracking-tight text-slate-900'>{card.value}</p> <p className='mt-4 text-3xl font-semibold tracking-tight text-slate-900 dark:text-white'>{card.value}</p>
<p className='mt-3 text-sm leading-6 text-slate-500'>{card.note}</p> <p className='mt-3 text-sm leading-6 text-slate-500 dark:text-slate-400'>{card.note}</p>
</div> </div>
<div> <div>
<BaseButton href={card.href} color='whiteDark' label='Open related records' /> <BaseButton href={card.href} color='whiteDark' label='Open related records' />
@ -510,16 +582,16 @@ const ExecutiveSummaryPage = () => {
const watchlistBlock = Boolean(data.workspace?.watchlistCards?.length) && ( const watchlistBlock = Boolean(data.workspace?.watchlistCards?.length) && (
<div className='mb-6 grid grid-cols-1 gap-4 xl:grid-cols-3'> <div className='mb-6 grid grid-cols-1 gap-4 xl:grid-cols-3'>
{data.workspace?.watchlistCards.map((card) => ( {data.workspace?.watchlistCards.map((card) => (
<CardBox key={card.key} className='border border-slate-200 bg-white'> <CardBox key={card.key} className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<div className='flex h-full flex-col justify-between gap-4'> <div className='flex h-full flex-col justify-between gap-4'>
<div> <div>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>Watchlist</p> <p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>Watchlist</p>
<h3 className='mt-2 text-lg font-semibold text-slate-900'>{card.title}</h3> <h3 className='mt-2 text-lg font-semibold text-slate-900 dark:text-white'>{card.title}</h3>
<div className='mt-4 flex items-end gap-3'> <div className='mt-4 flex items-end gap-3'>
<p className='text-3xl font-semibold tracking-tight text-slate-900'>{card.count}</p> <p className='text-3xl font-semibold tracking-tight text-slate-900 dark:text-white'>{card.count}</p>
<p className='pb-1 text-sm text-slate-500'>records in view</p> <p className='pb-1 text-sm text-slate-500 dark:text-slate-400'>records in view</p>
</div> </div>
<p className='mt-3 text-sm leading-6 text-slate-500'>{card.note}</p> <p className='mt-3 text-sm leading-6 text-slate-500 dark:text-slate-400'>{card.note}</p>
</div> </div>
<div> <div>
<BaseButton href={card.href} color='whiteDark' label='Open watchlist' /> <BaseButton href={card.href} color='whiteDark' label='Open watchlist' />
@ -532,7 +604,7 @@ const ExecutiveSummaryPage = () => {
const approvalRiskBlock = ( const approvalRiskBlock = (
<div className='mb-6 grid grid-cols-1 gap-6 xl:grid-cols-[1.55fr,1fr]'> <div className='mb-6 grid grid-cols-1 gap-6 xl:grid-cols-[1.55fr,1fr]'>
<CardBox className='border border-slate-200 bg-white'> <CardBox className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<SectionHeader <SectionHeader
eyebrow={workspaceConfig.sectionCopy.approvalQueue.eyebrow} eyebrow={workspaceConfig.sectionCopy.approvalQueue.eyebrow}
title={workspaceConfig.sectionCopy.approvalQueue.title} title={workspaceConfig.sectionCopy.approvalQueue.title}
@ -546,12 +618,12 @@ const ExecutiveSummaryPage = () => {
/> />
{loading ? ( {loading ? (
<div className='py-10 text-sm text-slate-500'>Loading approval workload</div> <div className='py-10 text-sm text-slate-500 dark:text-slate-400'>Loading approval workload</div>
) : data.approvalQueue.length ? ( ) : data.approvalQueue.length ? (
<div className='mt-4 overflow-x-auto'> <div className='mt-4 overflow-x-auto'>
<table className='min-w-full divide-y divide-slate-200 text-sm'> <table className='min-w-full divide-y divide-slate-200 text-sm'>
<thead> <thead>
<tr className='text-left text-xs uppercase tracking-[0.16em] text-slate-500'> <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> <th className='py-3 pr-4 font-semibold'>Workflow</th>
<th className='py-3 pr-4 font-semibold'>Step</th> <th className='py-3 pr-4 font-semibold'>Step</th>
<th className='py-3 pr-4 font-semibold'>Requested by</th> <th className='py-3 pr-4 font-semibold'>Requested by</th>
@ -564,25 +636,25 @@ const ExecutiveSummaryPage = () => {
{data.approvalQueue.map((item) => ( {data.approvalQueue.map((item) => (
<tr key={item.id} className='align-top'> <tr key={item.id} className='align-top'>
<td className='py-4 pr-4'> <td className='py-4 pr-4'>
<div className='font-medium text-slate-900'>{item.workflowName}</div> <div className='font-medium text-slate-900 dark:text-white'>{item.workflowName}</div>
<div className='mt-1'> <div className='mt-1'>
<span className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(item.status)}`}> <span className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(item.status)}`}>
{humanize(item.status)} {humanize(item.status)}
</span> </span>
</div> </div>
</td> </td>
<td className='py-4 pr-4 text-slate-600'> <td className='py-4 pr-4 text-slate-600 dark:text-slate-300'>
{item.stepOrder ? `Step ${item.stepOrder}` : '—'} · {item.stepName} {item.stepOrder ? `Step ${item.stepOrder}` : '—'} · {item.stepName}
</td> </td>
<td className='py-4 pr-4 text-slate-600'>{item.requestedBy}</td> <td className='py-4 pr-4 text-slate-600 dark:text-slate-300'>{item.requestedBy}</td>
<td className='py-4 pr-4 text-slate-600'>{item.assignedTo}</td> <td className='py-4 pr-4 text-slate-600 dark:text-slate-300'>{item.assignedTo}</td>
<td className='py-4 pr-4 text-slate-600'>{formatDate(item.requestedAt)}</td> <td className='py-4 pr-4 text-slate-600 dark:text-slate-300'>{formatDate(item.requestedAt)}</td>
<td className='py-4 pr-4'> <td className='py-4 pr-4'>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<Link href={`/approvals/approvals-view?id=${item.id}`} className='font-medium text-blue-700 hover:text-blue-900'> <Link href={`/approvals/approvals-view?id=${item.id}`} className='font-medium text-blue-700 hover:text-blue-900'>
Open approval Open approval
</Link> </Link>
<Link href={getRecordLink(item.recordType, item.recordKey)} className='text-slate-600 hover:text-slate-900'> <Link href={getRecordLink(item.recordType, item.recordKey)} className='text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:text-white'>
{humanize(item.recordType)} record {humanize(item.recordType)} record
</Link> </Link>
</div> </div>
@ -593,25 +665,25 @@ const ExecutiveSummaryPage = () => {
</table> </table>
</div> </div>
) : ( ) : (
<div className='mt-5 rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500'> <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'>
No pending approvals are queued right now. Start a new requisition to test the workflow end to end. No pending approvals are queued right now. Start a new requisition to test the workflow end to end.
</div> </div>
)} )}
</CardBox> </CardBox>
<CardBox className='border border-slate-200 bg-white'> <CardBox className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<SectionHeader <SectionHeader
eyebrow={workspaceConfig.sectionCopy.riskPanel.eyebrow} eyebrow={workspaceConfig.sectionCopy.riskPanel.eyebrow}
title={workspaceConfig.sectionCopy.riskPanel.title} title={workspaceConfig.sectionCopy.riskPanel.title}
action={ action={
<span className='rounded-md border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'> <span className='rounded-md border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>
{data.summary.openRiskAlerts} open alerts {data.summary.openRiskAlerts} open alerts
</span> </span>
} }
/> />
<div className='mt-4 grid gap-3'> <div className='mt-4 grid gap-3'>
<div className='rounded-md border border-red-100 bg-red-50 p-4'> <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='text-sm font-semibold text-red-800'>Budget headroom</p>
<p className='mt-2 text-lg font-semibold text-red-950'> <p className='mt-2 text-lg font-semibold text-red-950'>
{formatCurrency(data.summary.budgetVariance.USD, 'USD')} / {formatCurrency(data.summary.budgetVariance.CDF, 'CDF')} {formatCurrency(data.summary.budgetVariance.USD, 'USD')} / {formatCurrency(data.summary.budgetVariance.CDF, 'CDF')}
@ -619,13 +691,13 @@ const ExecutiveSummaryPage = () => {
<p className='mt-1 text-sm text-red-700'>Remaining variance between approved allocations and current commitments.</p> <p className='mt-1 text-sm text-red-700'>Remaining variance between approved allocations and current commitments.</p>
</div> </div>
<div className='grid grid-cols-2 gap-3'> <div className='grid grid-cols-2 gap-3'>
<div className='rounded-md border border-amber-200 bg-amber-50 p-4'> <div className='rounded-md border border-amber-200 bg-amber-50 p-4 dark:border-amber-900/60 dark:bg-amber-950/30'>
<p className='text-2xl font-semibold text-amber-900'>{data.summary.overduePayments}</p> <p className='text-2xl font-semibold text-amber-900'>{data.summary.overduePayments}</p>
<p className='mt-1 text-sm text-amber-800'>Overdue payment requests</p> <p className='mt-1 text-sm text-amber-800'>Overdue payment requests</p>
</div> </div>
<div className='rounded-md border border-slate-200 bg-slate-50 p-4'> <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-2xl font-semibold text-slate-900'>{data.summary.highRiskProjects}</p> <p className='text-2xl font-semibold text-slate-900 dark:text-white'>{data.summary.highRiskProjects}</p>
<p className='mt-1 text-sm text-slate-700'>High-risk projects</p> <p className='mt-1 text-sm text-slate-700 dark:text-slate-300'>High-risk projects</p>
</div> </div>
</div> </div>
</div> </div>
@ -633,12 +705,12 @@ const ExecutiveSummaryPage = () => {
<div className='mt-5 space-y-3'> <div className='mt-5 space-y-3'>
{data.riskPanel.length ? ( {data.riskPanel.length ? (
data.riskPanel.slice(0, 4).map((risk) => ( 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'> <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'>
<div className='flex items-start justify-between gap-3'> <div className='flex items-start justify-between gap-3'>
<div> <div>
<p className='font-medium text-slate-900'>{risk.title}</p> <p className='font-medium text-slate-900 dark:text-white'>{risk.title}</p>
<p className='mt-1 text-sm text-slate-600'>{risk.details || humanize(risk.alertType)}</p> <p className='mt-1 text-sm text-slate-600 dark:text-slate-300'>{risk.details || humanize(risk.alertType)}</p>
<p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400'>Assigned to {risk.assignedTo}</p> <p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400 dark:text-slate-500'>Assigned to {risk.assignedTo}</p>
</div> </div>
<span className={`inline-flex rounded-md border px-2 py-1 text-xs font-semibold ${severityBadgeClass(risk.severity)}`}> <span className={`inline-flex rounded-md border px-2 py-1 text-xs font-semibold ${severityBadgeClass(risk.severity)}`}>
{humanize(risk.severity)} {humanize(risk.severity)}
@ -647,7 +719,7 @@ const ExecutiveSummaryPage = () => {
</Link> </Link>
)) ))
) : ( ) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500'> <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 open compliance alerts are currently active. No open compliance alerts are currently active.
</div> </div>
)} )}
@ -658,7 +730,7 @@ const ExecutiveSummaryPage = () => {
const operationsBlock = ( const operationsBlock = (
<div className='mb-6 grid grid-cols-1 gap-6 xl:grid-cols-[1.2fr,1fr,1fr]'> <div className='mb-6 grid grid-cols-1 gap-6 xl:grid-cols-[1.2fr,1fr,1fr]'>
<CardBox className='border border-slate-200 bg-white'> <CardBox className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<SectionHeader <SectionHeader
eyebrow={workspaceConfig.sectionCopy.procurementQueue.eyebrow} eyebrow={workspaceConfig.sectionCopy.procurementQueue.eyebrow}
title={workspaceConfig.sectionCopy.procurementQueue.title} title={workspaceConfig.sectionCopy.procurementQueue.title}
@ -672,7 +744,7 @@ const ExecutiveSummaryPage = () => {
/> />
<div className='mt-4 space-y-3'> <div className='mt-4 space-y-3'>
<div className='grid gap-2 rounded-md border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600'> <div className='grid gap-2 rounded-md border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/70 text-sm text-slate-600 dark:text-slate-300'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<span className='flex h-6 w-6 items-center justify-center rounded-full bg-slate-900 text-xs font-semibold text-white'>1</span> <span className='flex h-6 w-6 items-center justify-center rounded-full bg-slate-900 text-xs font-semibold text-white'>1</span>
Requisition created Requisition created
@ -697,37 +769,37 @@ const ExecutiveSummaryPage = () => {
{data.procurementQueue.length ? ( {data.procurementQueue.length ? (
data.procurementQueue.map((item) => ( data.procurementQueue.map((item) => (
<Link href={`/requisitions/requisitions-view?id=${item.id}`} key={item.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50'> <Link href={`/requisitions/requisitions-view?id=${item.id}`} key={item.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 className='flex items-start justify-between gap-3'>
<div> <div>
<p className='font-medium text-slate-900'>{item.requisitionNumber || 'Requisition'}</p> <p className='font-medium text-slate-900 dark:text-white'>{item.requisitionNumber || 'Requisition'}</p>
<p className='mt-1 text-sm text-slate-700'>{item.title}</p> <p className='mt-1 text-sm text-slate-700 dark:text-slate-300'>{item.title}</p>
<p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400'> <p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400 dark:text-slate-500'>
{humanize(item.procurementMethod)} · {item.provinceName} {humanize(item.procurementMethod)} · {item.provinceName}
</p> </p>
</div> </div>
<div className='text-right'> <div className='text-right'>
<p className='text-sm font-semibold text-slate-900'>{formatCurrency(item.estimatedAmount, item.currency)}</p> <p className='text-sm font-semibold text-slate-900 dark:text-white'>{formatCurrency(item.estimatedAmount, item.currency)}</p>
<span className={`mt-2 inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(item.status)}`}> <span className={`mt-2 inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(item.status)}`}>
{humanize(item.status)} {humanize(item.status)}
</span> </span>
</div> </div>
</div> </div>
<div className='mt-3 flex items-center justify-between text-xs text-slate-500'> <div className='mt-3 flex items-center justify-between text-xs text-slate-500 dark:text-slate-400'>
<span>Needed by {formatDate(item.neededByDate)}</span> <span>Needed by {formatDate(item.neededByDate)}</span>
<span className='font-medium text-blue-700'>Open record</span> <span className='font-medium text-blue-700'>Open record</span>
</div> </div>
</Link> </Link>
)) ))
) : ( ) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500'> <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 live requisitions were found. Use New requisition to begin the procurement chain. No live requisitions were found. Use New requisition to begin the procurement chain.
</div> </div>
)} )}
</div> </div>
</CardBox> </CardBox>
<CardBox className='border border-slate-200 bg-white'> <CardBox className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<SectionHeader <SectionHeader
eyebrow={workspaceConfig.sectionCopy.contractWatchlist.eyebrow} eyebrow={workspaceConfig.sectionCopy.contractWatchlist.eyebrow}
title={workspaceConfig.sectionCopy.contractWatchlist.title} title={workspaceConfig.sectionCopy.contractWatchlist.title}
@ -742,38 +814,38 @@ const ExecutiveSummaryPage = () => {
<div className='mt-4 space-y-3'> <div className='mt-4 space-y-3'>
{data.contractWatchlist.length ? ( {data.contractWatchlist.length ? (
data.contractWatchlist.map((contract) => ( data.contractWatchlist.map((contract) => (
<Link href={`/contracts/contracts-view?id=${contract.id}`} key={contract.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50'> <Link href={`/contracts/contracts-view?id=${contract.id}`} key={contract.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 className='flex items-start justify-between gap-3'>
<div> <div>
<p className='font-medium text-slate-900'>{contract.contractNumber || 'Contract record'}</p> <p className='font-medium text-slate-900 dark:text-white'>{contract.contractNumber || 'Contract record'}</p>
<p className='mt-1 text-sm text-slate-700'>{contract.title}</p> <p className='mt-1 text-sm text-slate-700 dark:text-slate-300'>{contract.title}</p>
<p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400'>{contract.vendorName}</p> <p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400 dark:text-slate-500'>{contract.vendorName}</p>
</div> </div>
<span className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(contract.status)}`}> <span className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(contract.status)}`}>
{humanize(contract.status)} {humanize(contract.status)}
</span> </span>
</div> </div>
<div className='mt-3 grid grid-cols-2 gap-3 text-sm text-slate-600'> <div className='mt-3 grid grid-cols-2 gap-3 text-sm text-slate-600 dark:text-slate-300'>
<div> <div>
<p className='text-xs uppercase tracking-[0.16em] text-slate-400'>End date</p> <p className='text-xs uppercase tracking-[0.16em] text-slate-400 dark:text-slate-500'>End date</p>
<p className='mt-1 font-medium text-slate-900'>{formatDate(contract.endDate)}</p> <p className='mt-1 font-medium text-slate-900 dark:text-white'>{formatDate(contract.endDate)}</p>
</div> </div>
<div> <div>
<p className='text-xs uppercase tracking-[0.16em] text-slate-400'>Days to expiry</p> <p className='text-xs uppercase tracking-[0.16em] text-slate-400 dark:text-slate-500'>Days to expiry</p>
<p className='mt-1 font-medium text-slate-900'>{contract.daysToExpiry ?? '—'}</p> <p className='mt-1 font-medium text-slate-900 dark:text-white'>{contract.daysToExpiry ?? '—'}</p>
</div> </div>
</div> </div>
</Link> </Link>
)) ))
) : ( ) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500'> <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 active contracts are expiring within the next 60 days. No active contracts are expiring within the next 60 days.
</div> </div>
)} )}
</div> </div>
</CardBox> </CardBox>
<CardBox className='border border-slate-200 bg-white'> <CardBox className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<SectionHeader <SectionHeader
eyebrow={workspaceConfig.sectionCopy.recentNotifications.eyebrow} eyebrow={workspaceConfig.sectionCopy.recentNotifications.eyebrow}
title={workspaceConfig.sectionCopy.recentNotifications.title} title={workspaceConfig.sectionCopy.recentNotifications.title}
@ -788,22 +860,22 @@ const ExecutiveSummaryPage = () => {
<div className='mt-4 space-y-3'> <div className='mt-4 space-y-3'>
{data.recentNotifications.length ? ( {data.recentNotifications.length ? (
data.recentNotifications.map((notification) => ( 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'> <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'>
<div className='flex items-start justify-between gap-3'> <div className='flex items-start justify-between gap-3'>
<div> <div>
<p className='font-medium text-slate-900'>{notification.title}</p> <p className='font-medium text-slate-900 dark:text-white'>{notification.title}</p>
<p className='mt-1 line-clamp-2 text-sm text-slate-600'>{notification.message}</p> <p className='mt-1 line-clamp-2 text-sm text-slate-600 dark:text-slate-300'>{notification.message}</p>
</div> </div>
<BaseIcon path={mdiBellOutline} size={18} className='text-slate-500' /> <BaseIcon path={mdiBellOutline} size={18} className='text-slate-500 dark:text-slate-400' />
</div> </div>
<div className='mt-3 flex items-center justify-between text-xs text-slate-500'> <div className='mt-3 flex items-center justify-between text-xs text-slate-500 dark:text-slate-400'>
<span>{humanize(notification.type)}</span> <span>{humanize(notification.type)}</span>
<span>{formatDate(notification.sentAt)}</span> <span>{formatDate(notification.sentAt)}</span>
</div> </div>
</Link> </Link>
)) ))
) : ( ) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500'> <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 recent notifications were returned for this user. No recent notifications were returned for this user.
</div> </div>
)} )}
@ -814,7 +886,7 @@ const ExecutiveSummaryPage = () => {
const deliveryBlock = ( const deliveryBlock = (
<div className='mb-6 grid grid-cols-1 gap-6 xl:grid-cols-[1fr,1fr]'> <div className='mb-6 grid grid-cols-1 gap-6 xl:grid-cols-[1fr,1fr]'>
<CardBox className='border border-slate-200 bg-white'> <CardBox className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<SectionHeader <SectionHeader
eyebrow={workspaceConfig.sectionCopy.provinceRollout.eyebrow} eyebrow={workspaceConfig.sectionCopy.provinceRollout.eyebrow}
title={workspaceConfig.sectionCopy.provinceRollout.title} title={workspaceConfig.sectionCopy.provinceRollout.title}
@ -832,14 +904,14 @@ const ExecutiveSummaryPage = () => {
<div key={province.provinceName} className='rounded-md border border-slate-200 p-4'> <div key={province.provinceName} className='rounded-md border border-slate-200 p-4'>
<div className='flex items-center justify-between gap-3'> <div className='flex items-center justify-between gap-3'>
<div> <div>
<p className='font-medium text-slate-900'>{province.provinceName}</p> <p className='font-medium text-slate-900 dark:text-white'>{province.provinceName}</p>
<p className='mt-1 text-sm text-slate-500'> <p className='mt-1 text-sm text-slate-500 dark:text-slate-400'>
{province.activeProjects} active of {province.totalProjects} total projects {province.activeProjects} active of {province.totalProjects} total projects
</p> </p>
</div> </div>
<div className='text-right'> <div className='text-right'>
<p className='text-sm font-semibold text-slate-900'>{province.averageCompletion}%</p> <p className='text-sm font-semibold text-slate-900 dark:text-white'>{province.averageCompletion}%</p>
<p className='text-xs uppercase tracking-[0.16em] text-slate-400'>Average completion</p> <p className='text-xs uppercase tracking-[0.16em] text-slate-400 dark:text-slate-500'>Average completion</p>
</div> </div>
</div> </div>
<div className='mt-3 h-2 rounded-full bg-slate-100'> <div className='mt-3 h-2 rounded-full bg-slate-100'>
@ -848,14 +920,14 @@ const ExecutiveSummaryPage = () => {
</div> </div>
)) ))
) : ( ) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500'> <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 data is available yet.
</div> </div>
)} )}
</div> </div>
</CardBox> </CardBox>
<CardBox className='border border-slate-200 bg-white'> <CardBox className='border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900'>
<SectionHeader <SectionHeader
eyebrow={workspaceConfig.sectionCopy.topContracts.eyebrow} eyebrow={workspaceConfig.sectionCopy.topContracts.eyebrow}
title={workspaceConfig.sectionCopy.topContracts.title} title={workspaceConfig.sectionCopy.topContracts.title}
@ -871,7 +943,7 @@ const ExecutiveSummaryPage = () => {
{data.topContracts.length ? ( {data.topContracts.length ? (
<table className='min-w-full divide-y divide-slate-200 text-sm'> <table className='min-w-full divide-y divide-slate-200 text-sm'>
<thead> <thead>
<tr className='text-left text-xs uppercase tracking-[0.16em] text-slate-500'> <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'>Contract</th> <th className='py-3 pr-4 font-semibold'>Contract</th>
<th className='py-3 pr-4 font-semibold'>Vendor</th> <th className='py-3 pr-4 font-semibold'>Vendor</th>
<th className='py-3 pr-4 font-semibold'>Project</th> <th className='py-3 pr-4 font-semibold'>Project</th>
@ -882,20 +954,20 @@ const ExecutiveSummaryPage = () => {
{data.topContracts.map((contract) => ( {data.topContracts.map((contract) => (
<tr key={contract.id}> <tr key={contract.id}>
<td className='py-4 pr-4'> <td className='py-4 pr-4'>
<Link href={`/contracts/contracts-view?id=${contract.id}`} className='font-medium text-slate-900 hover:text-blue-700'> <Link href={`/contracts/contracts-view?id=${contract.id}`} className='font-medium text-slate-900 dark:text-white hover:text-blue-700'>
{contract.contractNumber || contract.title} {contract.contractNumber || contract.title}
</Link> </Link>
<p className='mt-1 text-slate-500'>{contract.title}</p> <p className='mt-1 text-slate-500 dark:text-slate-400'>{contract.title}</p>
</td> </td>
<td className='py-4 pr-4 text-slate-600'>{contract.vendorName}</td> <td className='py-4 pr-4 text-slate-600 dark:text-slate-300'>{contract.vendorName}</td>
<td className='py-4 pr-4 text-slate-600'>{contract.projectName}</td> <td className='py-4 pr-4 text-slate-600 dark:text-slate-300'>{contract.projectName}</td>
<td className='py-4 pr-4 font-medium text-slate-900'>{formatCurrency(contract.contractValue, contract.currency)}</td> <td className='py-4 pr-4 font-medium text-slate-900 dark:text-white'>{formatCurrency(contract.contractValue, contract.currency)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
) : ( ) : (
<div className='rounded-md border border-dashed border-slate-300 bg-slate-50 p-6 text-sm text-slate-500'> <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 contract commitments are available yet.
</div> </div>
)} )}
@ -907,18 +979,29 @@ const ExecutiveSummaryPage = () => {
const actionsBlock = ( const actionsBlock = (
<CardBox className='border border-slate-200 bg-slate-50'> <CardBox className='border border-slate-200 bg-slate-50'>
<div className='mb-4'> <div className='mb-4'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>{workspaceConfig.sectionCopy.quickActions.eyebrow}</p> <p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>{workspaceConfig.sectionCopy.quickActions.eyebrow}</p>
<h3 className='mt-1 text-xl font-semibold text-slate-900'>{workspaceConfig.sectionCopy.quickActions.title}</h3> <h3 className='mt-1 text-xl font-semibold text-slate-900 dark:text-white'>{workspaceConfig.sectionCopy.quickActions.title}</h3>
</div> </div>
<div className='grid gap-4 lg:grid-cols-4'> <div className='grid gap-4 lg:grid-cols-4'>
{workspaceConfig.quickLinks.map((link) => ( {workspaceConfig.quickLinks.map((link) => (
<Link href={link.href} key={`${link.href}-${link.label}`} className='rounded-md border border-slate-200 bg-white p-4 transition hover:border-slate-300 hover:bg-slate-100'> <Link
<div className='flex items-center gap-3'> href={link.href}
<BaseIcon path={actionIconMap[link.icon]} size={18} className='text-slate-700' /> key={`${link.href}-${link.label}`}
<div> className='group rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition duration-150 hover:-translate-y-0.5 hover:border-slate-300 hover:bg-slate-50'
<p className='font-medium text-slate-900'>{link.label}</p> >
<p className='text-sm text-slate-500'>{link.description}</p> <div className='flex h-full flex-col gap-4'>
<div className='flex items-start gap-3'>
<div className='flex h-11 w-11 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-slate-700 dark:text-slate-300 transition group-hover:border-slate-300 group-hover:bg-slate-100'>
<BaseIcon path={actionIconMap[link.icon]} size={18} className='text-current' />
</div> </div>
<div>
<p className='font-medium text-slate-900 dark:text-white'>{link.label}</p>
<p className='mt-1 text-sm leading-6 text-slate-500 dark:text-slate-400'>{link.description}</p>
</div>
</div>
<span className='inline-flex w-fit rounded-full border border-slate-200 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400 transition group-hover:border-slate-300 group-hover:text-slate-700 dark:text-slate-300'>
Open records
</span>
</div> </div>
</Link> </Link>
))} ))}
@ -949,36 +1032,59 @@ const ExecutiveSummaryPage = () => {
</BaseButtons> </BaseButtons>
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox className='mb-6 border border-slate-200 bg-slate-950 text-slate-100'> <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.9fr,1fr]'> <div className='grid gap-6 lg:grid-cols-[1.7fr,1fr]'>
<div> <div>
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-slate-400'>{workspaceConfig.eyebrow}</p> <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> <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='mt-4 max-w-3xl text-sm leading-6 text-slate-300'>{workspaceConfig.heroDescription}</p>
<div className='mt-5 flex flex-wrap gap-3 text-sm text-slate-300'> <div className='mt-6 grid gap-3 sm:grid-cols-2'>
{heroMetricChips.map((metric) => ( {heroMetricChips.map((metric) => (
<span key={metric.title} className='rounded-md border border-slate-800 bg-slate-900 px-3 py-2'> <div key={metric.title} className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
{metric.title}: {metric.value} <p className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500'>{metric.title}</p>
</span> <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>
))} ))}
</div> </div>
</div> </div>
<div className='grid gap-3 text-sm'> <div className='grid gap-4 text-sm'>
<div className='rounded-md border border-slate-800 bg-slate-900/80 p-4'> <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'>User context</p> <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-2 text-lg font-semibold text-white'>{currentUser?.firstName || currentUser?.email || 'Authenticated user'}</p>
<p className='mt-1 text-slate-400'>{data.workspace?.roleName || currentUser?.app_role?.name || 'Operational access'}</p> <p className='mt-1 text-slate-400 dark:text-slate-500'>{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'>
{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'>
{workspaceConfig.blockOrder.length} focus sections
</span>
</div> </div>
<div className='rounded-md border border-slate-800 bg-slate-900/80 p-4'> </div>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400'>Control indicators</p> <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='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>
</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>
</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='mt-3 grid grid-cols-2 gap-3'> <div className='mt-3 grid grid-cols-2 gap-3'>
<div> <div>
<p className='text-2xl font-semibold text-white'>{data.summary.overduePayments}</p> <p className='text-2xl font-semibold text-white'>{data.summary.overduePayments}</p>
<p className='text-slate-400'>Overdue payment requests</p> <p className='text-slate-400 dark:text-slate-500'>Overdue payment requests</p>
</div> </div>
<div> <div>
<p className='text-2xl font-semibold text-white'>{data.summary.unreadNotifications}</p> <p className='text-2xl font-semibold text-white'>{data.summary.unreadNotifications}</p>
<p className='text-slate-400'>Unread notifications</p> <p className='text-slate-400 dark:text-slate-500'>Unread notifications</p>
</div> </div>
</div> </div>
</div> </div>
@ -988,6 +1094,14 @@ const ExecutiveSummaryPage = () => {
{errorMessage && <NotificationBar color='danger'>{errorMessage}</NotificationBar>} {errorMessage && <NotificationBar color='danger'>{errorMessage}</NotificationBar>}
{workspaceBriefingCards.length ? (
<div className='mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4'>
{workspaceBriefingCards.map((card) => (
<RoleBriefingCard key={card.title} title={card.title} items={card.items} />
))}
</div>
) : null}
{workspaceConfig.blockOrder.map((blockKey) => { {workspaceConfig.blockOrder.map((blockKey) => {
const block = blockMap[blockKey]; const block = blockMap[blockKey];

View File

@ -3,6 +3,36 @@ import * as styles from '../styles'
import { localStorageDarkModeKey, localStorageStyleKey } from '../config' import { localStorageDarkModeKey, localStorageStyleKey } from '../config'
import { StyleKey } from '../interfaces' import { StyleKey } from '../interfaces'
const getInitialDarkMode = () => {
if (typeof window === 'undefined') {
return true
}
const storedDarkMode = localStorage.getItem(localStorageDarkModeKey)
if (storedDarkMode === '0') {
return false
}
if (storedDarkMode === '1') {
return true
}
return true
}
const syncDarkModeDocument = (darkMode: boolean) => {
if (typeof document === 'undefined') {
return
}
document.body.classList[darkMode ? 'add' : 'remove']('dark-scrollbars')
document.documentElement.classList[darkMode ? 'add' : 'remove']('dark-scrollbars-compat')
}
const initialDarkMode = getInitialDarkMode()
syncDarkModeDocument(initialDarkMode)
interface StyleState { interface StyleState {
asideStyle: string asideStyle: string
asideScrollbarsStyle: string asideScrollbarsStyle: string
@ -42,7 +72,7 @@ const initialState: StyleState = {
navBarItemLabelHoverStyle: styles.white.navBarItemLabelHover, navBarItemLabelHoverStyle: styles.white.navBarItemLabelHover,
navBarItemLabelActiveColorStyle: styles.white.navBarItemLabelActiveColor, navBarItemLabelActiveColorStyle: styles.white.navBarItemLabelActiveColor,
overlayStyle: styles.white.overlay, overlayStyle: styles.white.overlay,
darkMode: false, darkMode: initialDarkMode,
bgLayoutColor: styles.white.bgLayoutColor, bgLayoutColor: styles.white.bgLayoutColor,
iconsColor: styles.white.iconsColor, iconsColor: styles.white.iconsColor,
activeLinkColor: styles.white.activeLinkColor, activeLinkColor: styles.white.activeLinkColor,
@ -70,13 +100,7 @@ export const styleSlice = createSlice({
localStorage.setItem(localStorageDarkModeKey, state.darkMode ? '1' : '0') localStorage.setItem(localStorageDarkModeKey, state.darkMode ? '1' : '0')
} }
if (typeof document !== 'undefined') { syncDarkModeDocument(state.darkMode)
document.body.classList[state.darkMode ? 'add' : 'remove']('dark-scrollbars')
document.documentElement.classList[state.darkMode ? 'add' : 'remove'](
'dark-scrollbars-compat'
)
}
}, },
setStyle: (state, action: PayloadAction<StyleKey>) => { setStyle: (state, action: PayloadAction<StyleKey>) => {

View File

@ -25,29 +25,29 @@ interface StyleObject {
} }
export const white: StyleObject = { export const white: StyleObject = {
aside: 'bg-white dark:text-white', aside: 'bg-white dark:bg-slate-950 dark:text-slate-100',
asideScrollbars: 'aside-scrollbars-light', asideScrollbars: 'aside-scrollbars-light',
asideBrand: '', asideBrand: 'border-b border-slate-200/80 dark:border-slate-800',
asideMenuItem: 'text-gray-700 hover:bg-gray-100/70 dark:text-dark-500 dark:hover:text-white dark:hover:bg-dark-800', asideMenuItem: 'text-slate-700 hover:bg-slate-100/80 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-900/80 dark:hover:text-white',
asideMenuItemActive: 'font-bold text-black dark:text-white', asideMenuItemActive: 'font-semibold text-slate-950 dark:text-white',
asideMenuDropdown: 'bg-gray-100/75', asideMenuDropdown: 'bg-slate-100/70 dark:bg-slate-950/40',
navBarItemLabel: 'text-blue-600', navBarItemLabel: 'text-blue-700 dark:text-slate-200',
navBarItemLabelHover: 'hover:text-black', navBarItemLabelHover: 'hover:text-slate-950 dark:hover:text-white',
navBarItemLabelActiveColor: 'text-black', navBarItemLabelActiveColor: 'text-slate-950 dark:text-white',
overlay: 'from-white via-gray-100 to-white', overlay: 'from-white via-gray-100 to-white dark:from-slate-950 dark:via-slate-900 dark:to-slate-950',
activeLinkColor: 'bg-gray-100/70', activeLinkColor: 'bg-slate-100/90 dark:bg-slate-900/90',
bgLayoutColor: 'bg-gray-50', bgLayoutColor: 'bg-slate-50 dark:bg-slate-950',
iconsColor: 'text-blue-500', iconsColor: 'text-blue-600 dark:text-sky-400',
cardsColor: 'bg-white', cardsColor: 'bg-white dark:bg-slate-900',
focusRingColor: 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none border-gray-300 dark:focus:ring-blue-600 dark:focus:border-blue-600', focusRingColor: 'focus:ring focus:ring-blue-600 focus:border-blue-600 focus:outline-none border-gray-300 dark:border-slate-700 dark:focus:ring-blue-500 dark:focus:border-blue-500',
corners: 'rounded', corners: 'rounded-xl',
cardsStyle: 'bg-white border border-pavitra-400', cardsStyle: 'bg-white border border-slate-200 shadow-sm dark:bg-slate-900 dark:border-slate-800 dark:shadow-none',
linkColor: 'text-blue-600', linkColor: 'text-blue-700 dark:text-sky-400',
websiteHeder: 'border-b border-gray-200', websiteHeder: 'border-b border-slate-200 dark:border-slate-800',
borders: 'border-gray-200', borders: 'border-slate-200 dark:border-slate-800',
shadow: '', shadow: 'shadow-sm shadow-slate-950/5',
websiteSectionStyle: '', websiteSectionStyle: '',
textSecondary: 'text-gray-500', textSecondary: 'text-slate-500 dark:text-slate-400',
} }