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 = {
[WORKSPACE_ROLES.superAdmin]: {
summaryMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'contractsNearingExpiry', 'unreadNotifications', 'vendorComplianceAlerts'],
summaryMetricKeys: ['pendingApprovals', 'openRiskAlerts', 'unreadNotifications', 'vendorComplianceAlerts', 'activeProjects', 'contractsNearingExpiry'],
focusCards: [
{
key: 'super-admin-approvals',
title: 'Workflow backlog requiring oversight',
title: 'Governance approvals waiting attention',
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',
},
{
key: 'super-admin-alerts',
title: 'Open risk exposure',
key: 'super-admin-risks',
title: 'Cross-organization control alerts',
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',
},
{
key: 'super-admin-projects',
title: 'Live execution footprint',
metricKey: 'activeProjects',
note: 'Projects currently moving through the institution and contributing to workload volume.',
href: '/projects/projects-list',
key: 'super-admin-notifications',
title: 'Unread platform notices',
metricKey: 'unreadNotifications',
note: 'Recent notices that can reveal governance, setup, or platform support issues.',
href: '/notifications/notifications-list',
},
{
key: 'super-admin-notifications',
title: 'Unread notices',
metricKey: 'unreadNotifications',
note: 'Unread workflow, control, and system notices that may hide operational friction.',
href: '/notifications/notifications-list',
key: 'super-admin-vendor-alerts',
title: 'Vendor compliance exposure',
metricKey: 'vendorComplianceAlerts',
note: 'Supplier evidence and expiry issues that may require top-level visibility or policy follow-up.',
href: '/vendor_compliance_documents/vendor_compliance_documents-list',
},
],
watchlistCards: [
{
key: 'super-admin-approval-queue',
title: 'Escalation approval inbox',
key: 'super-admin-approval-watch',
title: 'Governance approval queue',
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',
},
{
key: 'super-admin-risk-watch',
title: 'Platform red-flag watchlist',
title: 'Control red-flag watchlist',
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',
},
{
key: 'super-admin-notification-watch',
title: 'Unread institutional notices',
key: 'super-admin-notice-watch',
title: 'Platform notice watchlist',
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',
},
],
},
[WORKSPACE_ROLES.administrator]: {
summaryMetricKeys: ['pendingApprovals', 'unreadNotifications', 'contractsNearingExpiry', 'openRiskAlerts', 'activeProjects', 'procurementPipeline'],
summaryMetricKeys: ['pendingApprovals', 'unreadNotifications', 'openRiskAlerts', 'vendorComplianceAlerts'],
focusCards: [
{
key: 'admin-approvals',
title: 'Approvals awaiting operational follow-through',
title: 'Approvals blocked by routing or delegation',
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',
},
{
key: 'admin-notifications',
title: 'Unread admin-facing notices',
title: 'Unread administrator notices',
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',
},
{
key: 'admin-contracts',
title: 'Contracts nearing renewal attention',
metricKey: 'contractsNearingExpiry',
note: 'Active contracts nearing expiry and likely to trigger administrative coordination.',
href: '/contracts/contracts-list',
key: 'admin-risks',
title: 'Administrative control signals',
metricKey: 'openRiskAlerts',
note: 'Open alerts that can indicate setup, delegation, or coordination problems affecting operations.',
href: '/compliance_alerts/compliance_alerts-list',
},
{
key: 'admin-pipeline',
title: 'Procurement workload in motion',
metricKey: 'procurementPipeline',
note: 'Requisitions advancing through review, tendering, or award that can pressure operations teams.',
href: '/requisitions/requisitions-list',
key: 'admin-vendor-alerts',
title: 'Evidence and document alerts',
metricKey: 'vendorComplianceAlerts',
note: 'Document and compliance issues that often need routing, notice, or administrator coordination.',
href: '/vendor_compliance_documents/vendor_compliance_documents-list',
},
],
watchlistCards: [
{
key: 'admin-approval-queue',
title: 'Operational approval queue',
key: 'admin-approval-watch',
title: 'Workflow routing queue',
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',
},
{
key: 'admin-procurement-queue',
title: 'Live procurement queue',
collectionKey: 'procurementQueue',
note: 'Requisitions that may be blocked by setup, documentation, or coordination issues.',
href: '/requisitions/requisitions-list',
key: 'admin-notification-watch',
title: 'Administrator notice watchlist',
collectionKey: 'recentNotifications',
note: 'Recent support and system signals that help spot process issues early.',
href: '/notifications/notifications-list',
},
{
key: 'admin-notification-watch',
title: 'Recent administrative notices',
collectionKey: 'recentNotifications',
note: 'Recent events to help admins spot support and process issues early.',
href: '/notifications/notifications-list',
key: 'admin-risk-watch',
title: 'Administrative exception watchlist',
collectionKey: 'riskPanel',
note: 'Open alerts and exceptions likely to require cleanup, coordination, or follow-through.',
href: '/compliance_alerts/compliance_alerts-list',
},
],
},
@ -201,112 +201,112 @@ const workspacePayloadConfigs = {
summaryMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects', 'contractsNearingExpiry'],
focusCards: [
{
key: 'dg-budget',
title: 'Approved budget envelope',
metricKey: 'approvedBudget',
note: 'Current allocation envelope available for strategic oversight and institutional steering.',
key: 'dg-commitments',
title: 'Committed budget requiring executive attention',
metricKey: 'committedBudget',
note: 'Current commitment pressure across the institution relative to approved funding space.',
href: '/allocations/allocations-list',
},
{
key: 'dg-disbursed',
title: 'Disbursements already released',
metricKey: 'disbursedBudget',
note: 'Payments processed so far, useful for comparing commitment and delivery momentum.',
href: '/payments/payments-list',
key: 'dg-approvals',
title: 'Executive decisions waiting',
metricKey: 'pendingApprovals',
note: 'High-impact approvals and escalations still waiting for institutional direction.',
href: '/approvals/approvals-list',
},
{
key: 'dg-risk-projects',
title: 'Projects under elevated risk',
key: 'dg-risks',
title: 'High-risk projects in view',
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',
},
{
key: 'dg-contracts',
title: 'Contracts nearing decision point',
title: 'Contracts nearing executive attention',
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',
},
],
watchlistCards: [
{
key: 'dg-contract-watch',
title: 'Executive contract watchlist',
collectionKey: 'contractWatchlist',
note: 'Contracts closest to expiry or requiring strategic follow-up.',
href: '/contracts/contracts-list',
key: 'dg-approval-watch',
title: 'Executive escalation queue',
collectionKey: 'approvalQueue',
note: 'Pending approvals and requests most likely to require executive direction.',
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',
title: 'Strategic risk watchlist',
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',
},
{
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]: {
summaryMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments', 'pendingApprovals', 'unreadNotifications'],
focusCards: [
{
key: 'finance-approved',
title: 'Approved budget base',
metricKey: 'approvedBudget',
note: 'The fiscal envelope currently approved and available for expenditure control.',
key: 'finance-commitments',
title: 'Committed budget under finance control',
metricKey: 'committedBudget',
note: 'Current commitments that finance must monitor against available funding and approval status.',
href: '/allocations/allocations-list',
},
{
key: 'finance-committed',
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',
key: 'finance-payments',
title: 'Aging payment requests',
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',
},
{
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: [
{
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',
title: 'Approvals blocking spend',
title: 'Finance approval queue',
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',
},
{
key: 'finance-contract-watch',
title: 'Contract value watchlist',
collectionKey: 'topContracts',
note: 'Largest contracts in scope for exposure review and budget tracking.',
title: 'Commitment watchlist',
collectionKey: 'contractWatchlist',
note: 'Contracts most likely to shape upcoming payment or budget pressure.',
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]: {
@ -314,53 +314,53 @@ const workspacePayloadConfigs = {
focusCards: [
{
key: 'procurement-pipeline',
title: 'Live requisition pipeline',
title: 'Requisitions moving through sourcing',
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',
},
{
key: 'procurement-approvals',
title: 'Approvals slowing procurement',
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',
},
{
key: 'procurement-contracts',
title: 'Contracts close to expiry',
title: 'Contracts nearing procurement follow-up',
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',
},
{
key: 'procurement-vendor-alerts',
title: 'Vendor compliance issues',
key: 'procurement-vendors',
title: 'Vendor readiness issues',
metricKey: 'vendorComplianceAlerts',
note: 'Open missing-document or expiry alerts that may block award or execution readiness.',
href: '/compliance_alerts/compliance_alerts-list',
note: 'Supplier document and compliance gaps that can slow award or contract execution.',
href: '/vendors/vendors-list',
},
],
watchlistCards: [
{
key: 'procurement-queue',
title: 'Priority procurement queue',
title: 'Priority sourcing queue',
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',
},
{
key: 'procurement-contract-watch',
title: 'Expiring contract watchlist',
title: 'Contract renewal watchlist',
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',
},
{
key: 'procurement-approval-watch',
title: 'Approval blockers to clear',
title: 'Procurement approval watchlist',
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',
},
],
@ -386,7 +386,7 @@ const workspacePayloadConfigs = {
key: 'compliance-contracts',
title: 'Contracts nearing obligation review',
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',
},
{
@ -407,16 +407,16 @@ const workspacePayloadConfigs = {
},
{
key: 'compliance-approval-watch',
title: 'Approvals with control exposure',
title: 'Approvals needing control review',
collectionKey: 'approvalQueue',
note: 'Pending approvals where evidence, delegation, or routing should be verified.',
href: '/approvals/approvals-list',
},
{
key: 'compliance-notification-watch',
title: 'Recent control notices',
title: 'Control notice watchlist',
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',
},
],
@ -435,7 +435,7 @@ const workspacePayloadConfigs = {
key: 'delivery-progress',
title: 'Average execution progress',
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',
},
{
@ -456,11 +456,18 @@ const workspacePayloadConfigs = {
watchlistCards: [
{
key: 'delivery-rollout-watch',
title: 'Province rollout snapshot',
title: 'Province delivery watchlist',
collectionKey: 'provinceRollout',
note: 'Provincial delivery concentration and completion spread across the current portfolio.',
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',
title: 'Delivery risk watchlist',
@ -468,13 +475,6 @@ const workspacePayloadConfigs = {
note: 'Open alerts most likely to disrupt project execution.',
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,
);
const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : ''
const isGroupTrigger = Boolean(item.menu && !item.href && !isDropdownList)
const { asPath, isReady } = useRouter()
@ -66,7 +67,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
<span
className={`grow text-ellipsis line-clamp-1 ${
item.menu ? '' : 'pr-12'
} ${activeClassAddon}`}
} ${isGroupTrigger ? 'text-[0.95rem] tracking-[0.01em]' : ''} ${activeClassAddon}`}
>
{item.label}
</span>
@ -81,18 +82,19 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
)
const componentClass = [
'flex cursor-pointer py-1.5 ',
isDropdownList ? 'px-6 text-sm' : '',
'group flex items-center gap-1 rounded-xl border border-transparent px-3 transition-all duration-150',
isDropdownList ? 'py-2 text-sm' : 'py-2.5',
item.color
? getButtonColor(item.color, false, true)
: `${asideMenuItemStyle}`,
isGroupTrigger ? 'font-semibold' : '',
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(' ');
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.href && (
<Link href={item.href} target={item.target} className={componentClass}>
@ -108,7 +110,9 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
<AsideMenuList
menu={item.menu}
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
/>

View File

@ -89,6 +89,11 @@ export interface WorkspaceSectionCopy {
actionLabel?: string;
}
export interface WorkspaceBriefingCard {
title: string;
items: string[];
}
export interface WorkspaceConfig {
sidebarLabel: string;
pageTitle: string;
@ -97,6 +102,7 @@ export interface WorkspaceConfig {
heroDescription: string;
primaryAction: WorkspaceAction;
secondaryAction: WorkspaceAction;
briefingCards: WorkspaceBriefingCard[];
highlightedMetricKeys: WorkspaceMetricKey[];
heroMetricKeys: WorkspaceMetricKey[];
blockOrder: WorkspaceDetailBlockKey[];
@ -104,11 +110,12 @@ export interface WorkspaceConfig {
quickLinks: WorkspaceQuickLink[];
}
type WorkspaceConfigInput = Omit<WorkspaceConfig, 'sectionCopy' | 'heroMetricKeys' | 'blockOrder' | 'quickLinks'> & {
type WorkspaceConfigInput = Omit<WorkspaceConfig, 'sectionCopy' | 'heroMetricKeys' | 'blockOrder' | 'quickLinks' | 'briefingCards'> & {
heroMetricKeys?: WorkspaceMetricKey[];
blockOrder?: WorkspaceDetailBlockKey[];
sectionCopy?: Partial<Record<WorkspaceSectionKey, WorkspaceSectionCopy>>;
quickLinks?: WorkspaceQuickLink[];
briefingCards?: WorkspaceBriefingCard[];
};
const defaultBlockOrder: WorkspaceDetailBlockKey[] = [
@ -194,6 +201,7 @@ const createWorkspaceConfig = ({
blockOrder,
sectionCopy,
quickLinks,
briefingCards,
...config
}: WorkspaceConfigInput): WorkspaceConfig => ({
...config,
@ -204,224 +212,291 @@ const createWorkspaceConfig = ({
...sectionCopy,
},
quickLinks: quickLinks || defaultQuickLinks,
briefingCards: briefingCards || [],
});
const workspaceConfigs: Record<string, WorkspaceConfig> = {
[WORKSPACE_ROLES.superAdmin]: createWorkspaceConfig({
sidebarLabel: 'Platform Command',
pageTitle: 'Platform Command Workspace',
eyebrow: 'FDSU ERP · Tenant governance workspace',
heroTitle: 'Govern tenants, access policy, institutional activity, and platform-level risk from one command surface.',
sidebarLabel: 'Platform Administration',
pageTitle: 'Platform Administration',
eyebrow: 'FDSU ERP · Platform Administration',
heroTitle: 'Set the platform structure, govern access, and watch cross-organization control risk.',
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.',
primaryAction: { href: '/organizations/organizations-list', label: 'Review tenant registry' },
secondaryAction: { href: '/users/users-list', label: 'Open access control' },
highlightedMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'contractsNearingExpiry', 'unreadNotifications', 'vendorComplianceAlerts'],
heroMetricKeys: ['activeProjects', 'pendingApprovals', 'openRiskAlerts', 'unreadNotifications'],
blockOrder: ['focus', 'watchlist', 'summary', 'approvalRisk', 'operations', 'delivery', 'actions'],
'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 organizations' },
secondaryAction: { href: '/roles/roles-list', label: 'Review roles' },
briefingCards: [
{
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: {
approvalQueue: {
eyebrow: 'Governance queue',
title: 'Escalations and approvals requiring governance attention',
title: 'Escalations and approvals that need platform-level attention',
actionLabel: 'Open governance queue',
},
riskPanel: {
eyebrow: 'Cross-cutting control flags',
title: 'Platform-wide exceptions, permission exposure, and institutional control risk',
eyebrow: 'Platform control exposure',
title: 'Cross-organization exceptions, access issues, and audit signals',
},
recentNotifications: {
eyebrow: 'Cross-tenant activity',
title: 'Recent workflow and control signals across organizations',
actionLabel: 'Open activity feed',
eyebrow: 'Cross-organization notices',
title: 'Recent notices that can reveal access, setup, or control problems',
actionLabel: 'Open notices',
},
quickActions: {
eyebrow: 'Platform actions',
title: 'Jump directly into tenant oversight and permission-control work',
eyebrow: 'Platform administration shortcuts',
title: 'Go directly to organizations, users, roles, and permissions',
},
},
quickLinks: [
{
href: '/organizations/organizations-list',
label: 'Organization registry',
description: 'Review tenant setup, coverage, and stewardship ownership.',
label: 'Organizations',
description: 'Review tenant setup, ownership, and coverage.',
icon: 'organizations',
},
{
href: '/users/users-list',
label: 'User access',
description: 'Manage accounts, authority assignments, and platform ownership.',
label: 'Users',
description: 'Manage top-level accounts and platform access.',
icon: 'users',
},
{
href: '/roles/roles-list',
label: 'Role catalog',
description: 'Govern platform-wide role definitions and responsibility boundaries.',
label: 'Roles',
description: 'Define responsibility boundaries across the platform.',
icon: 'users',
},
{
href: '/permissions/permissions-list',
label: 'Permission matrix',
description: 'Inspect and tighten permission coverage across the platform.',
label: 'Permissions',
description: 'Inspect and tighten the permission matrix.',
icon: 'audit',
},
],
}),
[WORKSPACE_ROLES.administrator]: createWorkspaceConfig({
sidebarLabel: 'Operations Command',
pageTitle: 'Operations Command Workspace',
eyebrow: 'FDSU ERP · Workflow operations workspace',
heroTitle: 'Run workflow operations, user support, record readiness, and day-to-day administrative follow-through.',
sidebarLabel: 'Organization Administration',
pageTitle: 'Organization Administration',
eyebrow: 'FDSU ERP · Organization Administration',
heroTitle: 'Keep the organization ready to operate: users, routing, notices, and master data.',
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.',
primaryAction: { href: '/approval_workflows/approval_workflows-list', label: 'Open workflow hub' },
secondaryAction: { href: '/notifications/notifications-list', label: 'Open notice center' },
highlightedMetricKeys: ['pendingApprovals', 'unreadNotifications', 'contractsNearingExpiry', 'openRiskAlerts', 'activeProjects', 'procurementPipeline'],
heroMetricKeys: ['pendingApprovals', 'unreadNotifications', 'procurementPipeline', 'contractsNearingExpiry'],
blockOrder: ['focus', 'summary', 'approvalRisk', 'watchlist', 'operations', 'delivery', 'actions'],
'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: 'Review workflow setup' },
secondaryAction: { href: '/notifications/notifications-list', label: 'Review notifications' },
briefingCards: [
{
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: {
approvalQueue: {
eyebrow: 'Operations queue',
title: 'Approvals waiting on routing, delegation, or admin follow-through',
actionLabel: 'Open workflow queue',
eyebrow: 'Workflow routing queue',
title: 'Approvals waiting because routing, delegation, or setup needs attention',
actionLabel: 'Open approval queue',
},
procurementQueue: {
eyebrow: 'Operational throughput',
title: 'Requisitions that may require workflow or setup unblock',
actionLabel: 'Open requisitions',
riskPanel: {
eyebrow: 'Administrative control signals',
title: 'Alerts and exceptions that often point to setup, delegation, or record issues',
},
recentNotifications: {
eyebrow: 'Operational notices',
title: 'Recent admin-facing signals requiring follow-through',
actionLabel: 'Open notice center',
eyebrow: 'Administrator notices',
title: 'Support and system notices requiring administrator follow-through',
actionLabel: 'Open notices',
},
quickActions: {
eyebrow: 'Administrative actions',
title: 'Open grouped control areas without scanning the full ERP register',
eyebrow: 'Administration shortcuts',
title: 'Go straight to users, workflow setup, notifications, and organization records',
},
},
quickLinks: [
{
href: '/users/users-list',
label: 'Users',
description: 'Manage onboarding, account support, and assignments.',
icon: 'users',
},
{
href: '/approval_workflows/approval_workflows-list',
label: 'Workflow hub',
description: 'Keep approval routes, steps, and queues ready for daily use.',
label: 'Approval workflows',
description: 'Maintain routes, steps, and approval readiness.',
icon: 'approvals',
},
{
href: '/notifications/notifications-list',
label: 'Notice center',
description: 'Triage notifications, documents, and control signals in one stop.',
label: 'Notifications',
description: 'Review notices, workflow signals, and support follow-up.',
icon: 'notifications',
},
{
href: '/users/users-list',
label: 'Admin records',
description: 'Jump into users, departments, provinces, and support clean-up.',
href: '/departments/departments-list',
label: 'Departments',
description: 'Keep organization structure and master data aligned.',
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({
sidebarLabel: 'Executive Command',
pageTitle: 'Director General Workspace',
eyebrow: 'FDSU ERP · Director General workspace',
heroTitle: 'Track strategic budget use, delivery momentum, risk concentration, and contract exposure across the institution.',
sidebarLabel: 'Executive Oversight',
pageTitle: 'Executive Oversight',
eyebrow: 'FDSU ERP · Executive Oversight',
heroTitle: 'See the institution across finance, procurement, compliance, and delivery, then make executive decisions.',
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.',
primaryAction: { href: '/projects/projects-list', label: 'Review projects' },
secondaryAction: { href: '/contracts/contracts-list', label: 'Review contracts' },
'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: '/approvals/approvals-list', label: 'Review escalations' },
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'],
heroMetricKeys: ['approvedBudget', 'disbursedBudget', 'pendingApprovals', 'highRiskProjects'],
blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'operations', 'actions'],
blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'actions'],
sectionCopy: {
riskPanel: {
eyebrow: 'Executive risk watch',
title: 'Strategic risks, bottlenecks, and control exposure',
title: 'Strategic risks, bottlenecks, and control exposure needing executive attention',
},
provinceRollout: {
eyebrow: 'Delivery coverage',
title: 'Provincial rollout and execution pace',
title: 'Project rollout and execution pace across provinces',
actionLabel: 'Open projects',
},
topContracts: {
eyebrow: 'Contract exposure',
title: 'Largest commitments shaping institutional delivery',
actionLabel: 'Vendor and contract view',
eyebrow: 'Major commitments',
title: 'Largest contracts shaping institutional exposure and delivery',
actionLabel: 'Open contracts',
},
quickActions: {
eyebrow: 'Executive actions',
title: 'Jump into the records most relevant for strategic follow-up',
eyebrow: 'Executive shortcuts',
title: 'Open the records most relevant to executive review and escalation',
},
},
quickLinks: [
{
href: '/approvals/approvals-list',
label: 'Escalations',
description: 'Review approvals and issues that need executive decision.',
icon: 'approvals',
},
{
href: '/projects/projects-list',
label: 'Project portfolio',
description: 'Review delivery status and ownership.',
label: 'Projects',
description: 'Review delivery status and accountability.',
icon: 'projects',
},
{
href: '/contracts/contracts-list',
label: 'Contract exposure',
description: 'Inspect major commitments and expiry dates.',
label: 'Contracts',
description: 'Inspect major commitments and expiry exposure.',
icon: 'contracts',
},
{
href: '/approvals/approvals-list',
label: 'Decision queue',
description: 'Track items waiting for institutional action.',
icon: 'approvals',
},
{
href: '/notifications/notifications-list',
label: 'Executive signals',
description: 'Review the latest alerts and escalations.',
label: 'Executive notices',
description: 'See the latest escalations and control signals.',
icon: 'notifications',
},
],
}),
[WORKSPACE_ROLES.financeDirector]: createWorkspaceConfig({
sidebarLabel: 'Financial Control',
pageTitle: 'Finance Director Workspace',
eyebrow: 'FDSU ERP · Finance Director workspace',
heroTitle: 'Monitor allocations, commitments, invoices, payments, and approval pressure with finance-first operational context.',
sidebarLabel: 'Financial Management',
pageTitle: 'Financial Management',
eyebrow: 'FDSU ERP · Financial Management',
heroTitle: 'Control budget, commitments, disbursements, and payment pressure.',
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' },
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'],
heroMetricKeys: ['approvedBudget', 'committedBudget', 'disbursedBudget', 'overduePayments'],
blockOrder: ['summary', 'watchlist', 'focus', 'approvalRisk', 'delivery', 'operations', 'actions'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Finance queue',
title: 'Approvals affecting commitments and disbursements',
eyebrow: 'Finance approval queue',
title: 'Approvals affecting commitments, disbursements, and payment timing',
actionLabel: 'Open finance approvals',
},
riskPanel: {
eyebrow: 'Fiscal control panel',
title: 'Budget pressure, overdue payments, and control exceptions',
},
topContracts: {
eyebrow: 'High-value commitments',
title: 'Largest contracts influencing current financial exposure',
actionLabel: 'Open vendor master',
eyebrow: 'Fiscal exposure',
title: 'Budget pressure, aging payments, and control exceptions affecting finance',
},
quickActions: {
eyebrow: 'Finance actions',
title: 'Move from budget control into payment and allocation follow-up',
eyebrow: 'Finance shortcuts',
title: 'Go directly to allocations, payment requests, commitments, and approvals',
},
},
quickLinks: [
{
href: '/payment_requests/payment_requests-list',
label: 'Payment requests',
description: 'Review overdue and pending disbursements.',
description: 'Review pending and aging disbursement requests.',
icon: 'payments',
},
{
@ -432,108 +507,144 @@ const workspaceConfigs: Record<string, WorkspaceConfig> = {
},
{
href: '/contracts/contracts-list',
label: 'Commitment register',
description: 'Inspect the contracts driving spend.',
label: 'Commitments',
description: 'Inspect the contracts driving current spend exposure.',
icon: 'contracts',
},
{
href: '/approvals/approvals-list',
label: 'Approval pressure',
label: 'Finance approvals',
description: 'Track bottlenecks holding back finance action.',
icon: 'approvals',
},
],
}),
[WORKSPACE_ROLES.procurementLead]: createWorkspaceConfig({
sidebarLabel: 'Procurement Desk',
pageTitle: 'Procurement Lead Workspace',
eyebrow: 'FDSU ERP · Procurement Lead workspace',
heroTitle: 'Stay on top of requisitions, tender flow, contract deadlines, and approval backlog across the procurement chain.',
sidebarLabel: 'Procurement Management',
pageTitle: 'Procurement Management',
eyebrow: 'FDSU ERP · Procurement Management',
heroTitle: 'Own the demand-to-award chain: requisitions, tenders, vendors, awards, and contract readiness.',
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' },
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'],
heroMetricKeys: ['procurementPipeline', 'pendingApprovals', 'contractsNearingExpiry', 'vendorComplianceAlerts'],
blockOrder: ['focus', 'summary', 'operations', 'watchlist', 'approvalRisk', 'delivery', 'actions'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'operations', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Procurement approvals',
title: 'Decisions blocking sourcing and award progress',
eyebrow: 'Procurement approval queue',
title: 'Decisions blocking sourcing, evaluation, award, or contract signature',
actionLabel: 'Open approval backlog',
},
procurementQueue: {
eyebrow: 'Sourcing pipeline',
title: 'Requisitions and tenders requiring procurement action',
title: 'Requisitions and tenders currently needing procurement action',
actionLabel: 'Open sourcing queue',
},
contractWatchlist: {
eyebrow: 'Contract renewals',
title: 'Supplier commitments nearing expiry or follow-up',
title: 'Supplier commitments nearing expiry or needing follow-through',
actionLabel: 'Open contracts',
},
quickActions: {
eyebrow: 'Procurement actions',
title: 'Jump into the next sourcing and contract-management tasks',
eyebrow: 'Procurement shortcuts',
title: 'Go directly to requisitions, tenders, vendors, and contract follow-up',
},
},
quickLinks: [
{
href: '/requisitions/requisitions-list',
label: 'Requisition queue',
description: 'Review demand and readiness to source.',
label: 'Requisitions',
description: 'Review new demand and sourcing readiness.',
icon: 'requisitions',
},
{
href: '/tenders/tenders-list',
label: 'Tender pipeline',
label: 'Tenders',
description: 'Follow active tenders and consultations.',
icon: 'tenders',
},
{
href: '/contracts/contracts-list',
label: 'Contract deadlines',
description: 'Track expiring commitments and supplier action.',
label: 'Contracts',
description: 'Track expiring commitments and follow-up actions.',
icon: 'contracts',
},
{
href: '/vendors/vendors-list',
label: 'Vendor register',
label: 'Vendors',
description: 'Inspect supplier readiness and history.',
icon: 'vendors',
},
],
}),
[WORKSPACE_ROLES.complianceAuditLead]: createWorkspaceConfig({
sidebarLabel: 'Compliance Desk',
pageTitle: 'Compliance & Audit Workspace',
eyebrow: 'FDSU ERP · Compliance & Audit workspace',
heroTitle: 'Surface red flags, document gaps, control exceptions, and approval exposure before they become institutional failures.',
sidebarLabel: 'Compliance & Audit',
pageTitle: 'Compliance & Audit',
eyebrow: 'FDSU ERP · Compliance & Audit',
heroTitle: 'Watch control breaches, evidence gaps, expiring obligations, and approvals that need independent review.',
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' },
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'],
heroMetricKeys: ['openRiskAlerts', 'vendorComplianceAlerts', 'contractsNearingExpiry', 'pendingApprovals'],
blockOrder: ['watchlist', 'focus', 'summary', 'approvalRisk', 'operations', 'delivery', 'actions'],
blockOrder: ['summary', 'focus', 'watchlist', 'approvalRisk', 'actions'],
sectionCopy: {
approvalQueue: {
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',
},
riskPanel: {
eyebrow: 'Compliance watch',
title: 'Exceptions, overdue items, and evidence gaps',
title: 'Exceptions, evidence gaps, overdue items, and red flags needing review',
},
recentNotifications: {
eyebrow: 'Alert traffic',
title: 'Recent system signals and policy-relevant activity',
actionLabel: 'Open alert feed',
eyebrow: 'Control notices',
title: 'Recent signals that may indicate policy, evidence, or routing problems',
actionLabel: 'Open notices',
},
quickActions: {
eyebrow: 'Audit actions',
title: 'Go straight to alerts, logs, and records needing review',
eyebrow: 'Compliance shortcuts',
title: 'Go directly to alerts, logs, approvals, and obligations needing review',
},
},
quickLinks: [
@ -557,49 +668,67 @@ const workspaceConfigs: Record<string, WorkspaceConfig> = {
},
{
href: '/contracts/contracts-list',
label: 'Contract obligations',
label: 'Obligations',
description: 'Check commitments nearing expiry or breach.',
icon: 'contracts',
},
],
}),
[WORKSPACE_ROLES.projectDeliveryLead]: createWorkspaceConfig({
sidebarLabel: 'Delivery Command',
pageTitle: 'Project Delivery Workspace',
eyebrow: 'FDSU ERP · Project Delivery workspace',
heroTitle: 'Keep execution moving by watching project progress, blocked approvals, payment lag, and field-level delivery risk.',
sidebarLabel: 'Project Delivery',
pageTitle: 'Project Delivery',
eyebrow: 'FDSU ERP · Project Delivery',
heroTitle: 'Keep projects moving by watching milestones, execution risk, approvals, and payment delays.',
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' },
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'],
heroMetricKeys: ['activeProjects', 'averageProjectProgress', 'highRiskProjects', 'pendingApprovals'],
blockOrder: ['summary', 'focus', 'delivery', 'approvalRisk', 'watchlist', 'operations', 'actions'],
blockOrder: ['summary', 'focus', 'watchlist', 'delivery', 'approvalRisk', 'actions'],
sectionCopy: {
approvalQueue: {
eyebrow: 'Execution blockers',
title: 'Approvals holding back project delivery',
eyebrow: 'Delivery blocker queue',
title: 'Approvals holding back project execution',
actionLabel: 'Open delivery blockers',
},
provinceRollout: {
eyebrow: 'Delivery heatmap',
title: 'Progress by province and execution footprint',
eyebrow: 'Delivery footprint',
title: 'Project progress and execution pace by province',
actionLabel: 'Open project register',
},
topContracts: {
eyebrow: 'Major delivery contracts',
title: 'Largest commitments underpinning project execution',
actionLabel: 'Open suppliers',
eyebrow: 'Delivery contracts',
title: 'Largest contracts underpinning execution and milestone delivery',
actionLabel: 'Open contracts',
},
quickActions: {
eyebrow: 'Delivery actions',
title: 'Move into the projects, milestones, and blockers that need attention',
eyebrow: 'Delivery shortcuts',
title: 'Go directly to projects, milestones, blockers, and supporting contracts',
},
},
quickLinks: [
{
href: '/projects/projects-list',
label: 'Project portfolio',
label: 'Projects',
description: 'Review implementation status and ownership.',
icon: 'projects',
},

View File

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

View File

@ -259,7 +259,7 @@ const statusBadgeClass = (status?: string) => {
case 'failed':
return 'bg-red-50 text-red-700 border-red-200';
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':
return 'bg-amber-50 text-amber-700 border-amber-200';
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;
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>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>{eyebrow}</p>
<h3 className='mt-1 text-xl font-semibold text-slate-900'>{title}</h3>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400'>{eyebrow}</p>
<h3 className='mt-1 text-xl font-semibold text-slate-900 dark:text-white'>{title}</h3>
</div>
{action}
</div>
@ -330,20 +330,88 @@ const SummaryMetric = ({
note: 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>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>{title}</p>
<p className='mt-3 text-2xl font-semibold text-slate-900'>{value}</p>
<p className='mt-2 text-sm text-slate-500'>{note}</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 dark:text-white'>{value}</p>
<p className='mt-2 text-sm text-slate-500 dark:text-slate-400'>{note}</p>
</div>
<div className='flex h-11 w-11 items-center justify-center rounded-md border border-slate-200 bg-slate-50'>
<BaseIcon path={icon} size={22} className='text-slate-700' />
<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 dark:text-slate-200' />
</div>
</div>
</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 router = useRouter();
const { currentUser } = useAppSelector((state) => state.auth);
@ -479,16 +547,20 @@ const ExecutiveSummaryPage = () => {
[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) && (
<div className='mb-6 grid grid-cols-1 gap-4 xl:grid-cols-4'>
{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>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>Role focus</p>
<h3 className='mt-2 text-lg font-semibold text-slate-900'>{card.title}</h3>
<p className='mt-4 text-3xl font-semibold tracking-tight text-slate-900'>{card.value}</p>
<p className='mt-3 text-sm leading-6 text-slate-500'>{card.note}</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 dark:text-white'>{card.title}</h3>
<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 dark:text-slate-400'>{card.note}</p>
</div>
<div>
<BaseButton href={card.href} color='whiteDark' label='Open related records' />
@ -510,16 +582,16 @@ const ExecutiveSummaryPage = () => {
const watchlistBlock = Boolean(data.workspace?.watchlistCards?.length) && (
<div className='mb-6 grid grid-cols-1 gap-4 xl:grid-cols-3'>
{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>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>Watchlist</p>
<h3 className='mt-2 text-lg font-semibold text-slate-900'>{card.title}</h3>
<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 dark:text-white'>{card.title}</h3>
<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='pb-1 text-sm text-slate-500'>records in view</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 dark:text-slate-400'>records in view</p>
</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>
<BaseButton href={card.href} color='whiteDark' label='Open watchlist' />
@ -532,7 +604,7 @@ const ExecutiveSummaryPage = () => {
const approvalRiskBlock = (
<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
eyebrow={workspaceConfig.sectionCopy.approvalQueue.eyebrow}
title={workspaceConfig.sectionCopy.approvalQueue.title}
@ -546,12 +618,12 @@ const ExecutiveSummaryPage = () => {
/>
{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 ? (
<div className='mt-4 overflow-x-auto'>
<table className='min-w-full divide-y divide-slate-200 text-sm'>
<thead>
<tr className='text-left text-xs uppercase tracking-[0.16em] text-slate-500'>
<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'>Step</th>
<th className='py-3 pr-4 font-semibold'>Requested by</th>
@ -564,25 +636,25 @@ const ExecutiveSummaryPage = () => {
{data.approvalQueue.map((item) => (
<tr key={item.id} className='align-top'>
<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'>
<span className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(item.status)}`}>
{humanize(item.status)}
</span>
</div>
</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}
</td>
<td className='py-4 pr-4 text-slate-600'>{item.requestedBy}</td>
<td className='py-4 pr-4 text-slate-600'>{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'>{item.requestedBy}</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 dark:text-slate-300'>{formatDate(item.requestedAt)}</td>
<td className='py-4 pr-4'>
<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'>
Open approval
</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
</Link>
</div>
@ -593,25 +665,25 @@ const ExecutiveSummaryPage = () => {
</table>
</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.
</div>
)}
</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
eyebrow={workspaceConfig.sectionCopy.riskPanel.eyebrow}
title={workspaceConfig.sectionCopy.riskPanel.title}
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
</span>
}
/>
<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='mt-2 text-lg font-semibold text-red-950'>
{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>
</div>
<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='mt-1 text-sm text-amber-800'>Overdue payment requests</p>
</div>
<div className='rounded-md border border-slate-200 bg-slate-50 p-4'>
<p className='text-2xl font-semibold text-slate-900'>{data.summary.highRiskProjects}</p>
<p className='mt-1 text-sm text-slate-700'>High-risk projects</p>
<div className='rounded-md border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/70'>
<p className='text-2xl font-semibold text-slate-900 dark:text-white'>{data.summary.highRiskProjects}</p>
<p className='mt-1 text-sm text-slate-700 dark:text-slate-300'>High-risk projects</p>
</div>
</div>
</div>
@ -633,12 +705,12 @@ const ExecutiveSummaryPage = () => {
<div className='mt-5 space-y-3'>
{data.riskPanel.length ? (
data.riskPanel.slice(0, 4).map((risk) => (
<Link href={getRecordLink(risk.recordType, risk.recordKey)} key={risk.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50'>
<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>
<p className='font-medium text-slate-900'>{risk.title}</p>
<p className='mt-1 text-sm text-slate-600'>{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='font-medium text-slate-900 dark:text-white'>{risk.title}</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 dark:text-slate-500'>Assigned to {risk.assignedTo}</p>
</div>
<span className={`inline-flex rounded-md border px-2 py-1 text-xs font-semibold ${severityBadgeClass(risk.severity)}`}>
{humanize(risk.severity)}
@ -647,7 +719,7 @@ const ExecutiveSummaryPage = () => {
</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.
</div>
)}
@ -658,7 +730,7 @@ const ExecutiveSummaryPage = () => {
const operationsBlock = (
<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
eyebrow={workspaceConfig.sectionCopy.procurementQueue.eyebrow}
title={workspaceConfig.sectionCopy.procurementQueue.title}
@ -672,7 +744,7 @@ const ExecutiveSummaryPage = () => {
/>
<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'>
<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
@ -697,37 +769,37 @@ const ExecutiveSummaryPage = () => {
{data.procurementQueue.length ? (
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>
<p className='font-medium text-slate-900'>{item.requisitionNumber || 'Requisition'}</p>
<p className='mt-1 text-sm text-slate-700'>{item.title}</p>
<p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400'>
<p className='font-medium text-slate-900 dark:text-white'>{item.requisitionNumber || 'Requisition'}</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 dark:text-slate-500'>
{humanize(item.procurementMethod)} · {item.provinceName}
</p>
</div>
<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)}`}>
{humanize(item.status)}
</span>
</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 className='font-medium text-blue-700'>Open record</span>
</div>
</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.
</div>
)}
</div>
</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
eyebrow={workspaceConfig.sectionCopy.contractWatchlist.eyebrow}
title={workspaceConfig.sectionCopy.contractWatchlist.title}
@ -742,38 +814,38 @@ const ExecutiveSummaryPage = () => {
<div className='mt-4 space-y-3'>
{data.contractWatchlist.length ? (
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>
<p className='font-medium text-slate-900'>{contract.contractNumber || 'Contract record'}</p>
<p className='mt-1 text-sm text-slate-700'>{contract.title}</p>
<p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400'>{contract.vendorName}</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 dark:text-slate-300'>{contract.title}</p>
<p className='mt-2 text-xs uppercase tracking-[0.16em] text-slate-400 dark:text-slate-500'>{contract.vendorName}</p>
</div>
<span className={`inline-flex rounded-md border px-2 py-1 text-xs font-medium ${statusBadgeClass(contract.status)}`}>
{humanize(contract.status)}
</span>
</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>
<p className='text-xs uppercase tracking-[0.16em] text-slate-400'>End date</p>
<p className='mt-1 font-medium text-slate-900'>{formatDate(contract.endDate)}</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 dark:text-white'>{formatDate(contract.endDate)}</p>
</div>
<div>
<p className='text-xs uppercase tracking-[0.16em] text-slate-400'>Days to expiry</p>
<p className='mt-1 font-medium text-slate-900'>{contract.daysToExpiry ?? '—'}</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 dark:text-white'>{contract.daysToExpiry ?? '—'}</p>
</div>
</div>
</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.
</div>
)}
</div>
</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
eyebrow={workspaceConfig.sectionCopy.recentNotifications.eyebrow}
title={workspaceConfig.sectionCopy.recentNotifications.title}
@ -788,22 +860,22 @@ const ExecutiveSummaryPage = () => {
<div className='mt-4 space-y-3'>
{data.recentNotifications.length ? (
data.recentNotifications.map((notification) => (
<Link href={getRecordLink(notification.recordType, notification.recordKey)} key={notification.id} className='block rounded-md border border-slate-200 p-4 transition hover:border-slate-300 hover:bg-slate-50'>
<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>
<p className='font-medium text-slate-900'>{notification.title}</p>
<p className='mt-1 line-clamp-2 text-sm text-slate-600'>{notification.message}</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 dark:text-slate-300'>{notification.message}</p>
</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 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>{formatDate(notification.sentAt)}</span>
</div>
</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.
</div>
)}
@ -814,7 +886,7 @@ const ExecutiveSummaryPage = () => {
const deliveryBlock = (
<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
eyebrow={workspaceConfig.sectionCopy.provinceRollout.eyebrow}
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 className='flex items-center justify-between gap-3'>
<div>
<p className='font-medium text-slate-900'>{province.provinceName}</p>
<p className='mt-1 text-sm text-slate-500'>
<p className='font-medium text-slate-900 dark:text-white'>{province.provinceName}</p>
<p className='mt-1 text-sm text-slate-500 dark:text-slate-400'>
{province.activeProjects} active of {province.totalProjects} total projects
</p>
</div>
<div className='text-right'>
<p className='text-sm font-semibold text-slate-900'>{province.averageCompletion}%</p>
<p className='text-xs uppercase tracking-[0.16em] text-slate-400'>Average completion</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 dark:text-slate-500'>Average completion</p>
</div>
</div>
<div className='mt-3 h-2 rounded-full bg-slate-100'>
@ -848,14 +920,14 @@ const ExecutiveSummaryPage = () => {
</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.
</div>
)}
</div>
</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
eyebrow={workspaceConfig.sectionCopy.topContracts.eyebrow}
title={workspaceConfig.sectionCopy.topContracts.title}
@ -871,7 +943,7 @@ const ExecutiveSummaryPage = () => {
{data.topContracts.length ? (
<table className='min-w-full divide-y divide-slate-200 text-sm'>
<thead>
<tr className='text-left text-xs uppercase tracking-[0.16em] text-slate-500'>
<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'>Vendor</th>
<th className='py-3 pr-4 font-semibold'>Project</th>
@ -882,20 +954,20 @@ const ExecutiveSummaryPage = () => {
{data.topContracts.map((contract) => (
<tr key={contract.id}>
<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}
</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 className='py-4 pr-4 text-slate-600'>{contract.vendorName}</td>
<td className='py-4 pr-4 text-slate-600'>{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 text-slate-600 dark:text-slate-300'>{contract.vendorName}</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 dark:text-white'>{formatCurrency(contract.contractValue, contract.currency)}</td>
</tr>
))}
</tbody>
</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.
</div>
)}
@ -907,18 +979,29 @@ const ExecutiveSummaryPage = () => {
const actionsBlock = (
<CardBox className='border border-slate-200 bg-slate-50'>
<div className='mb-4'>
<p className='text-xs font-semibold uppercase tracking-[0.18em] text-slate-500'>{workspaceConfig.sectionCopy.quickActions.eyebrow}</p>
<h3 className='mt-1 text-xl font-semibold text-slate-900'>{workspaceConfig.sectionCopy.quickActions.title}</h3>
<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 dark:text-white'>{workspaceConfig.sectionCopy.quickActions.title}</h3>
</div>
<div className='grid gap-4 lg:grid-cols-4'>
{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'>
<div className='flex items-center gap-3'>
<BaseIcon path={actionIconMap[link.icon]} size={18} className='text-slate-700' />
<div>
<p className='font-medium text-slate-900'>{link.label}</p>
<p className='text-sm text-slate-500'>{link.description}</p>
<Link
href={link.href}
key={`${link.href}-${link.label}`}
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'
>
<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>
<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>
</Link>
))}
@ -949,36 +1032,59 @@ const ExecutiveSummaryPage = () => {
</BaseButtons>
</SectionTitleLineWithButton>
<CardBox className='mb-6 border border-slate-200 bg-slate-950 text-slate-100'>
<div className='grid gap-6 lg:grid-cols-[1.9fr,1fr]'>
<CardBox className='mb-6 overflow-hidden border border-slate-200 bg-gradient-to-br from-slate-950 via-slate-900 to-slate-800 text-slate-100 shadow-sm'>
<div className='grid gap-6 lg:grid-cols-[1.7fr,1fr]'>
<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>
<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) => (
<span key={metric.title} className='rounded-md border border-slate-800 bg-slate-900 px-3 py-2'>
{metric.title}: {metric.value}
</span>
<div key={metric.title} className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
<p className='text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500'>{metric.title}</p>
<p className='mt-2 text-2xl font-semibold text-white'>{metric.value}</p>
<p className='mt-2 text-xs leading-5 text-slate-400 dark:text-slate-500'>{metric.note}</p>
</div>
))}
</div>
</div>
<div className='grid gap-3 text-sm'>
<div className='rounded-md border border-slate-800 bg-slate-900/80 p-4'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400'>User context</p>
<div className='grid gap-4 text-sm'>
<div className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500'>Logged-in role</p>
<p className='mt-2 text-lg font-semibold text-white'>{currentUser?.firstName || currentUser?.email || 'Authenticated user'}</p>
<p className='mt-1 text-slate-400'>{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 className='rounded-md border border-slate-800 bg-slate-900/80 p-4'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400'>Control indicators</p>
</div>
<div className='rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm'>
<p className='text-xs uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500'>Role interconnection</p>
<div className='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>
<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>
<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>
@ -988,6 +1094,14 @@ const ExecutiveSummaryPage = () => {
{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) => {
const block = blockMap[blockKey];

View File

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

View File

@ -25,29 +25,29 @@ interface 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',
asideBrand: '',
asideMenuItem: 'text-gray-700 hover:bg-gray-100/70 dark:text-dark-500 dark:hover:text-white dark:hover:bg-dark-800',
asideMenuItemActive: 'font-bold text-black dark:text-white',
asideMenuDropdown: 'bg-gray-100/75',
navBarItemLabel: 'text-blue-600',
navBarItemLabelHover: 'hover:text-black',
navBarItemLabelActiveColor: 'text-black',
overlay: 'from-white via-gray-100 to-white',
activeLinkColor: 'bg-gray-100/70',
bgLayoutColor: 'bg-gray-50',
iconsColor: 'text-blue-500',
cardsColor: 'bg-white',
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',
corners: 'rounded',
cardsStyle: 'bg-white border border-pavitra-400',
linkColor: 'text-blue-600',
websiteHeder: 'border-b border-gray-200',
borders: 'border-gray-200',
shadow: '',
asideBrand: 'border-b border-slate-200/80 dark:border-slate-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-semibold text-slate-950 dark:text-white',
asideMenuDropdown: 'bg-slate-100/70 dark:bg-slate-950/40',
navBarItemLabel: 'text-blue-700 dark:text-slate-200',
navBarItemLabelHover: 'hover:text-slate-950 dark:hover:text-white',
navBarItemLabelActiveColor: 'text-slate-950 dark:text-white',
overlay: 'from-white via-gray-100 to-white dark:from-slate-950 dark:via-slate-900 dark:to-slate-950',
activeLinkColor: 'bg-slate-100/90 dark:bg-slate-900/90',
bgLayoutColor: 'bg-slate-50 dark:bg-slate-950',
iconsColor: 'text-blue-600 dark:text-sky-400',
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:border-slate-700 dark:focus:ring-blue-500 dark:focus:border-blue-500',
corners: 'rounded-xl',
cardsStyle: 'bg-white border border-slate-200 shadow-sm dark:bg-slate-900 dark:border-slate-800 dark:shadow-none',
linkColor: 'text-blue-700 dark:text-sky-400',
websiteHeder: 'border-b border-slate-200 dark:border-slate-800',
borders: 'border-slate-200 dark:border-slate-800',
shadow: 'shadow-sm shadow-slate-950/5',
websiteSectionStyle: '',
textSecondary: 'text-gray-500',
textSecondary: 'text-slate-500 dark:text-slate-400',
}