From 8ceeef10510665d04c5fd5e990468e98d777d059 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 4 Apr 2026 17:14:44 +0000 Subject: [PATCH] Autosave: 20260404-171444 --- backend/src/routes/executive_summary.js | 97 +- frontend/src/components/AsideMenu.tsx | 2 +- frontend/src/components/AsideMenuItem.tsx | 49 +- frontend/src/components/AsideMenuLayer.tsx | 134 +- frontend/src/css/_app.css | 2 +- frontend/src/layouts/Authenticated.tsx | 128 +- frontend/src/menuAside.ts | 333 +-- frontend/src/pages/dashboard.tsx | 2133 +++++--------------- frontend/src/pages/executive-summary.tsx | 204 +- 9 files changed, 957 insertions(+), 2125 deletions(-) diff --git a/backend/src/routes/executive_summary.js b/backend/src/routes/executive_summary.js index b661ba6..d142b60 100644 --- a/backend/src/routes/executive_summary.js +++ b/backend/src/routes/executive_summary.js @@ -1,4 +1,5 @@ const express = require('express'); +const validator = require('validator'); const db = require('../db/models'); const wrapAsync = require('../helpers').wrapAsync; @@ -67,6 +68,27 @@ const SUPPORTED_RECORD_TYPES = new Set([ 'compliance_alerts', ]); +const MIN_MAJOR_CONTRACT_VALUE = 1000; + +const hasMeaningfulText = (value) => Boolean(String(value || '').trim()); + +const getRecordId = (value) => { + const normalizedValue = String(value || '').trim(); + + if (!normalizedValue || !validator.isUUID(normalizedValue)) { + return null; + } + + return normalizedValue; +}; + +const getUserDisplayName = (user, fallback = '') => { + if (!user) { + return fallback; + } + + return `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.email || fallback; +}; const formatCurrencyValue = (value, currency) => new Intl.NumberFormat(currency === 'CDF' ? 'fr-CD' : 'en-US', { @@ -813,7 +835,11 @@ router.get( ]); const provinceRolloutMap = rolloutProjects.reduce((accumulator, project) => { - const provinceName = project.province?.name || 'Province not assigned'; + const provinceName = project.province?.name; + + if (!provinceName) { + return accumulator; + } if (!accumulator[provinceName]) { accumulator[provinceName] = { @@ -847,23 +873,27 @@ router.get( .slice(0, 6); const formattedApprovalQueue = approvalQueue - .filter((approval) => SUPPORTED_RECORD_TYPES.has(approval.record_type)) + .filter( + (approval) => + SUPPORTED_RECORD_TYPES.has(approval.record_type) + && hasMeaningfulText(approval.record_key) + && hasMeaningfulText(approval.workflow?.name) + && hasMeaningfulText(approval.step?.name) + && Boolean(getUserDisplayName(approval.requested_by_user)), + ) .map((approval) => ({ - id: approval.id, - recordType: approval.record_type, - recordKey: approval.record_key, - status: approval.status, - requestedAt: approval.requested_at, - workflowName: approval.workflow?.name || 'Workflow not configured', - stepName: approval.step?.name || 'Pending step setup', - stepOrder: approval.step?.step_order || null, - requestedBy: approval.requested_by_user - ? `${approval.requested_by_user.firstName || ''} ${approval.requested_by_user.lastName || ''}`.trim() || approval.requested_by_user.email - : 'Requester unavailable', - assignedTo: approval.assigned_to_user - ? `${approval.assigned_to_user.firstName || ''} ${approval.assigned_to_user.lastName || ''}`.trim() || approval.assigned_to_user.email - : 'Not assigned', - })); + id: approval.id, + recordType: approval.record_type, + recordKey: approval.record_key, + status: approval.status, + requestedAt: approval.requested_at, + recordId: getRecordId(approval.record_key), + workflowName: approval.workflow.name, + stepName: approval.step.name, + stepOrder: approval.step?.step_order || null, + requestedBy: getUserDisplayName(approval.requested_by_user), + assignedTo: getUserDisplayName(approval.assigned_to_user, 'Awaiting assignment'), + })); const formattedProcurementQueue = procurementQueue.map((requisition) => ({ id: requisition.id, @@ -895,18 +925,23 @@ router.get( })); const formattedTopContracts = topContracts - .filter((contract) => contract.vendor?.name || contract.project?.name) + .filter( + (contract) => + (contract.vendor?.name || contract.project?.name) + && hasMeaningfulText(contract.contract_number || contract.title) + && toNumber(contract.contract_value) >= MIN_MAJOR_CONTRACT_VALUE, + ) .map((contract) => ({ - id: contract.id, - contractNumber: contract.contract_number, - title: contract.title, - contractValue: toNumber(contract.contract_value), - currency: contract.currency, - endDate: contract.end_date, - status: contract.status, - vendorName: contract.vendor?.name || 'Vendor not linked', - projectName: contract.project?.name || 'Project not linked', - })); + id: contract.id, + contractNumber: contract.contract_number, + title: contract.title, + contractValue: toNumber(contract.contract_value), + currency: contract.currency, + endDate: contract.end_date, + status: contract.status, + vendorName: contract.vendor?.name || 'Vendor not linked', + projectName: contract.project?.name || 'Project not linked', + })); const formattedRiskPanel = riskPanel .filter((alert) => !alert.record_type || SUPPORTED_RECORD_TYPES.has(alert.record_type)) @@ -918,11 +953,10 @@ router.get( details: alert.details, recordType: alert.record_type, recordKey: alert.record_key, + recordId: getRecordId(alert.record_key), dueAt: alert.due_at, status: alert.status, - assignedTo: alert.assigned_to_user - ? `${alert.assigned_to_user.firstName || ''} ${alert.assigned_to_user.lastName || ''}`.trim() || alert.assigned_to_user.email - : 'Not assigned', + assignedTo: getUserDisplayName(alert.assigned_to_user, 'Unassigned'), })); const formattedNotifications = recentNotifications @@ -936,6 +970,7 @@ router.get( sentAt: notification.sent_at, recordType: notification.record_type, recordKey: notification.record_key, + recordId: getRecordId(notification.record_key), })); const summary = { diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx index 442dfac..11e58a3 100644 --- a/frontend/src/components/AsideMenu.tsx +++ b/frontend/src/components/AsideMenu.tsx @@ -19,7 +19,7 @@ export default function AsideMenu({ <> { const asideMenuItemStyle = useAppSelector((state) => state.style.asideMenuItemStyle) const asideMenuDropdownStyle = useAppSelector((state) => state.style.asideMenuDropdownStyle) const asideMenuItemActiveStyle = useAppSelector((state) => state.style.asideMenuItemActiveStyle) - const borders = useAppSelector((state) => state.style.borders); - const activeLinkColor = useAppSelector( - (state) => state.style.activeLinkColor, - ); + const borders = useAppSelector((state) => state.style.borders) + const activeLinkColor = useAppSelector((state) => state.style.activeLinkColor) const activeClassAddon = !item.color && isLinkActive ? asideMenuItemActiveStyle : '' const isGroupTrigger = Boolean(item.menu && !item.href && !isDropdownList) @@ -62,56 +60,51 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { const asideMenuItemInnerContents = ( <> {item.icon && ( - + + + )} {item.label} {item.menu && ( )} ) const componentClass = [ - 'group flex items-center gap-1 rounded-xl border border-transparent px-3 transition-all duration-150', + 'group flex w-full items-start gap-3 rounded-xl px-3 text-left transition-all duration-150', isDropdownList ? 'py-2 text-sm' : 'py-2.5', - item.color - ? getButtonColor(item.color, false, true) - : `${asideMenuItemStyle}`, + item.color ? getButtonColor(item.color, false, true) : `${asideMenuItemStyle}`, isGroupTrigger ? 'font-semibold' : '', - isLinkActive - ? `text-slate-950 ${activeLinkColor} shadow-sm ring-1 ring-inset ring-slate-200/80 dark:text-white dark:bg-slate-900 dark:ring-slate-800` - : 'hover:border-slate-200/80 dark:hover:border-slate-800/80', - ].join(' '); + isLinkActive ? `${activeLinkColor} shadow-sm` : '', + ].join(' ') return ( -
  • +
  • {item.withDevider &&
    } - {item.href && ( + {item.href ? ( {asideMenuItemInnerContents} - )} - {!item.href && ( -
    setIsDropdownActive(!isDropdownActive)}> + ) : ( +
    + )} {item.menu && ( void } +type OrganizationOption = { + id: string + name: string +} + export default function AsideMenuLayer({ menu, className = '', ...props }: Props) { - const corners = useAppSelector((state) => state.style.corners); + const corners = useAppSelector((state) => state.style.corners) const asideStyle = useAppSelector((state) => state.style.asideStyle) const asideBrandStyle = useAppSelector((state) => state.style.asideBrandStyle) const asideScrollbarsStyle = useAppSelector((state) => state.style.asideScrollbarsStyle) const darkMode = useAppSelector((state) => state.style.darkMode) + const { currentUser } = useAppSelector((state) => state.auth) + + const [organizations, setOrganizations] = React.useState([]) + + const roleName = currentUser?.app_role?.name || '' + const workspaceConfig = getWorkspaceConfig(roleName) + const workspaceHref = getWorkspaceRoute(roleName) + const organizationId = currentUser?.organizations?.id + + React.useEffect(() => { + let mounted = true + + const fetchOrganizations = async () => { + try { + const response = await axios.get('/org-for-auth') + + if (mounted) { + setOrganizations(Array.isArray(response.data) ? response.data : []) + } + } catch (error: any) { + console.error('Failed to load organizations for sidebar', error?.response || error) + } + } + + fetchOrganizations() + + return () => { + mounted = false + } + }, []) const handleAsideLgCloseClick = (e: React.MouseEvent) => { e.preventDefault() props.onAsideLgCloseClick() } - const dispatch = useAppDispatch(); - const { currentUser } = useAppSelector((state) => state.auth); - const organizationsId = currentUser?.organizations?.id; - const [organizations, setOrganizations] = React.useState(null); + const organizationName = React.useMemo(() => { + const matchedOrganization = organizations.find((item) => item.id === organizationId)?.name - const fetchOrganizations = createAsyncThunk('/org-for-auth', async () => { - try { - const response = await axios.get('/org-for-auth'); - setOrganizations(response.data); - return response.data; - } catch (error) { - console.error(error.response); - throw error; + if (!matchedOrganization) { + return 'Organization workspace' } - }); - - React.useEffect(() => { - dispatch(fetchOrganizations()); - }, [dispatch]); - - let organizationName = organizations?.find(item => item.id === organizationsId)?.name; - if(organizationName?.length > 25){ - organizationName = organizationName?.substring(0, 25) + '...'; - } + return matchedOrganization.length > 26 ? `${matchedOrganization.substring(0, 26)}...` : matchedOrganization + }, [organizationId, organizations]) return (