From a646f110df04c4b989916cab120aed7e9f7175df Mon Sep 17 00:00:00 2001 From: harish Date: Wed, 24 Sep 2025 16:04:07 -0400 Subject: [PATCH 1/8] progress --- .../ProjectViewContainer/ProjectDetails.tsx | 2 + .../ProjectPage/ProjectSpendingHistory.tsx | 127 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx index fd58452942..08f05e9b33 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx @@ -20,6 +20,7 @@ import LoadingIndicator from '../../../components/LoadingIndicator'; import PieChart from '../../FinancePage/FinanceComponents/PieChart'; import WarningBanner from '../../../components/WarningBanner'; import { Box } from '@mui/system'; +import ProjectSpendingHistory from '../../ProjectPage/ProjectSpendingHistory'; export const getProjectTeamsName = (project: ProjectPreview): string => { return project.teams.map((team) => team.teamName).join(', '); @@ -136,6 +137,7 @@ const ProjectDetails: React.FC = ({ project }) => { reimbursed={rrData.reimbursed} available={rrData.available} /> + )} diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx new file mode 100644 index 0000000000..b16fa1f6c3 --- /dev/null +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, + Collapse, + IconButton +} from '@mui/material'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import { useGetMaterialsForWbsElement } from '../../hooks/bom.hooks'; +import { Material, WbsNumber } from 'shared'; + +interface ProjectSpendingHistoryProps { + wbsNum: WbsNumber; +} + +const ProjectSpendingHistory: React.FC = ({ wbsNum }) => { + const { data: materials, isLoading, isError } = useGetMaterialsForWbsElement(wbsNum); + const [openRows, setOpenRows] = React.useState>({}); + + // Group materials by reimbursementRequestId + const grouped = React.useMemo(() => { + if (!materials) return []; + const map: Record = {}; + materials.forEach((mat) => { + const rr = mat.reimbursementRequest; + if (rr) { + if (!map[rr.reimbursementRequestId]) { + map[rr.reimbursementRequestId] = { request: rr, materials: [] }; + } + map[rr.reimbursementRequestId].materials.push(mat); + } + }); + return Object.values(map); + }, [materials]); + + if (isLoading) return Loading spending history...; + if (isError) return Failed to load spending history.; + if (!grouped.length) return No spending history for this project.; + + const handleToggleRow = (id: string) => { + setOpenRows((prev) => ({ ...prev, [id]: !prev[id] })); + }; + + return ( + + + Spending History + + + + + + + Submitter + Date + Status + Total Amount + + + + {grouped.map(({ request, materials }) => ( + + + + handleToggleRow(request.reimbursementRequestId)}> + {openRows[request.reimbursementRequestId] ? : } + + + {request.recipient?.name || request.recipient?.email || 'N/A'} + {new Date(request.dateCreated).toLocaleDateString()} + + + + ${request.totalCost?.toFixed(2) || '0.00'} + + + + + + + Line Items + +
+ + + Name + Notes + Amount + + + + {materials.map((mat) => ( + + {mat.name} + {mat.notes || '-'} + ${mat.price?.toFixed(2) || '0.00'} + + ))} + +
+
+ + + + + ))} + + + + + ); +}; + +export default ProjectSpendingHistory; From 60954c7f8d105f05c6dc2f04ebc201aec6cedabf Mon Sep 17 00:00:00 2001 From: harish Date: Thu, 25 Sep 2025 17:35:40 -0400 Subject: [PATCH 2/8] #3604 new tab --- .../ProjectViewContainer/ProjectDetails.tsx | 1 - .../ProjectViewContainer/ProjectViewContainer.tsx | 8 ++++++-- .../src/pages/ProjectPage/ProjectSpendingHistory.tsx | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx index 08f05e9b33..f4fa0ad92e 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx @@ -137,7 +137,6 @@ const ProjectDetails: React.FC = ({ project }) => { reimbursed={rrData.reimbursed} available={rrData.available} /> -
)} diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx index 83882f0969..c72d3993ce 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx @@ -34,6 +34,7 @@ import { useGetMaterialsForWbsElement } from '../../../hooks/bom.hooks'; import ChangeRequestTab from '../../../components/ChangeRequestTab'; import PartsReviewPage from './PartReview/PartsReviewPage'; import ActionsMenu from '../../../components/ActionsMenu'; +import ProjectSpendingHistory from '../../ProjectPage/ProjectSpendingHistory'; interface ProjectViewContainerProps { project: Project; @@ -179,7 +180,8 @@ const ProjectViewContainer: React.FC = ({ project, en { tabUrlValue: 'changes', tabName: 'Changes' }, { tabUrlValue: 'gantt', tabName: 'Gantt' }, { tabUrlValue: 'change-requests', tabName: 'Change Requests' }, - { tabUrlValue: 'parts-review', tabName: 'Parts Review' } + { tabUrlValue: 'parts-review', tabName: 'Parts Review' }, + { tabUrlValue: 'spending', tabName: 'Spending History' } ]} baseUrl={`${routes.PROJECTS}/${wbsNum}`} defaultTab="overview" @@ -202,8 +204,10 @@ const ProjectViewContainer: React.FC = ({ project, en ) : tab === 6 ? ( - ) : ( + ) : tab === 7 ? ( + ) : ( + )} {deleteModalShow && ( diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index b16fa1f6c3..82aab37180 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -26,7 +26,6 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum const { data: materials, isLoading, isError } = useGetMaterialsForWbsElement(wbsNum); const [openRows, setOpenRows] = React.useState>({}); - // Group materials by reimbursementRequestId const grouped = React.useMemo(() => { if (!materials) return []; const map: Record = {}; From a6d160df260b191f170368cd167b18952a931846 Mon Sep 17 00:00:00 2001 From: harish Date: Mon, 6 Oct 2025 17:02:59 -0400 Subject: [PATCH 3/8] filtering --- .../FinanceComponents/PieChart.tsx | 16 +- .../ProjectViewContainer.tsx | 2 +- .../ProjectPage/ProjectSpendingHistory.tsx | 438 ++++++++++++++---- 3 files changed, 361 insertions(+), 95 deletions(-) diff --git a/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx b/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx index f80e336eaa..2020650785 100644 --- a/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx @@ -24,10 +24,12 @@ const FinancePieChart: React.FC = ({ available }) => { const [isLegendOpen, setIsLegendOpen] = useState(true); + + // Combine pending categories into one + const pendingReimbursement = pendingLeadership + pendingFinance + submittedToSABO; + const [sectionStates, setSectionStates] = useState([ - { title: 'Pending Leadership', color: '#562016', expanded: false }, - { title: 'Pending Finance', color: '#8e3c2d', expanded: false }, - { title: 'Submitted to SABO', color: '#dd514c', expanded: false }, + { title: 'Pending Reimbursement', color: '#8e3c2d', expanded: false }, { title: 'Reimbursed', color: '#797a7a', expanded: false }, { title: 'Available', color: '#afafaf', expanded: false } ]); @@ -35,9 +37,7 @@ const FinancePieChart: React.FC = ({ const MIN_PERCENTAGE = 0.05; const data = [ - { name: 'Pending Leadership', value: pendingLeadership }, - { name: 'Pending Finance', value: pendingFinance }, - { name: 'Submitted to SABO', value: submittedToSABO }, + { name: 'Pending Reimbursement', value: pendingReimbursement }, { name: 'Reimbursed', value: reimbursed }, { name: 'Available', value: available } ]; @@ -73,9 +73,7 @@ const FinancePieChart: React.FC = ({ } const sectionColorMap = new Map([ - ['Pending Leadership', '#562016'], - ['Pending Finance', '#8e3c2d'], - ['Submitted to SABO', '#dd514c'], + ['Pending Reimbursement', '#8e3c2d'], ['Reimbursed', '#797a7a'], ['Available', '#afafaf'] ]); diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx index c72d3993ce..819915ca0e 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectViewContainer.tsx @@ -181,7 +181,7 @@ const ProjectViewContainer: React.FC = ({ project, en { tabUrlValue: 'gantt', tabName: 'Gantt' }, { tabUrlValue: 'change-requests', tabName: 'Change Requests' }, { tabUrlValue: 'parts-review', tabName: 'Parts Review' }, - { tabUrlValue: 'spending', tabName: 'Spending History' } + { tabUrlValue: 'spending', tabName: 'Budget' } ]} baseUrl={`${routes.PROJECTS}/${wbsNum}`} defaultTab="overview" diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index 82aab37180..3f4b478c07 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useMemo } from 'react'; import { Box, Typography, @@ -11,35 +11,174 @@ import { Paper, Chip, Collapse, - IconButton + IconButton, + TextField, + Grid, + MenuItem, + FormControl, + InputLabel, + Select, + Button, + Link } from '@mui/material'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import ClearIcon from '@mui/icons-material/Clear'; import { useGetMaterialsForWbsElement } from '../../hooks/bom.hooks'; -import { Material, WbsNumber } from 'shared'; +import { useAllReimbursementRequests } from '../../hooks/finance.hooks'; +import { useSingleProject } from '../../hooks/projects.hooks'; +import { Material, WbsNumber, ReimbursementRequest, WBSElementData, OtherProductReason, equalsWbsNumber } from 'shared'; interface ProjectSpendingHistoryProps { wbsNum: WbsNumber; } const ProjectSpendingHistory: React.FC = ({ wbsNum }) => { - const { data: materials, isLoading, isError } = useGetMaterialsForWbsElement(wbsNum); - const [openRows, setOpenRows] = React.useState>({}); - - const grouped = React.useMemo(() => { - if (!materials) return []; - const map: Record = {}; - materials.forEach((mat) => { - const rr = mat.reimbursementRequest; - if (rr) { - if (!map[rr.reimbursementRequestId]) { - map[rr.reimbursementRequestId] = { request: rr, materials: [] }; + const { data: materials, isLoading: materialsLoading, isError: materialsError } = useGetMaterialsForWbsElement(wbsNum); + const { data: allReimbursementRequests, isLoading: rrLoading, isError: rrError } = useAllReimbursementRequests(); + const { data: project, isLoading: projectLoading } = useSingleProject(wbsNum); + const [openRows, setOpenRows] = useState>({}); + const [showFilters, setShowFilters] = useState(false); + + // Filter states + const [submitterFilter, setSubmitterFilter] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + const [dateFromFilter, setDateFromFilter] = useState(''); + const [dateToFilter, setDateToFilter] = useState(''); + const [amountMinFilter, setAmountMinFilter] = useState(''); + const [amountMaxFilter, setAmountMaxFilter] = useState(''); + + const grouped = useMemo(() => { + if (!allReimbursementRequests || !project) return []; + + // Create a map of reimbursement requests that include both: + // 1. Requests with BOM materials for this project + // 2. Requests with products linked directly to this project + const requestMap = new Map(); + + // First, add reimbursement requests from BOM materials + if (materials) { + materials.forEach((mat) => { + const rr = mat.reimbursementRequest; + if (rr) { + if (!requestMap.has(rr.reimbursementRequestId)) { + requestMap.set(rr.reimbursementRequestId, { request: rr, materials: [] }); + } + requestMap.get(rr.reimbursementRequestId)!.materials.push(mat); } - map[rr.reimbursementRequestId].materials.push(mat); + }); + } + + // Then, add standalone reimbursement requests linked to this project + allReimbursementRequests.forEach((rr) => { + const hasProjectProduct = rr.reimbursementProducts.some((product) => { + const reason = product.reimbursementProductReason; + // Check if it's a WBS element and matches our project + if ((reason as WBSElementData).wbsNum) { + return equalsWbsNumber( + { ...(reason as WBSElementData).wbsNum, workPackageNumber: 0 }, // Convert to project WBS + wbsNum + ); + } + return false; + }); + + if (hasProjectProduct && !requestMap.has(rr.reimbursementRequestId)) { + requestMap.set(rr.reimbursementRequestId, { request: rr, materials: [] }); } }); - return Object.values(map); - }, [materials]); + + return Array.from(requestMap.values()); + }, [materials, allReimbursementRequests, project, wbsNum]); + + // Filter the grouped data + const filteredData = useMemo(() => { + return grouped.filter(({ request }) => { + // Submitter filter + if (submitterFilter) { + const submitterName = `${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email || ''; + if (!submitterName.toLowerCase().includes(submitterFilter.toLowerCase())) { + return false; + } + } + + // Status filter + if (statusFilter) { + const currentStatus = request.reimbursementStatuses?.[0]?.type || ''; + if (currentStatus !== statusFilter) { + return false; + } + } + + // Date range filter + const requestDate = new Date(request.dateCreated); + if (dateFromFilter) { + const fromDate = new Date(dateFromFilter); + if (requestDate < fromDate) { + return false; + } + } + if (dateToFilter) { + const toDate = new Date(dateToFilter); + toDate.setHours(23, 59, 59, 999); // End of day + if (requestDate > toDate) { + return false; + } + } + + // Amount range filter + const amount = (request.totalCost || 0) / 100; + if (amountMinFilter) { + const minAmount = parseFloat(amountMinFilter); + if (!isNaN(minAmount) && amount < minAmount) { + return false; + } + } + if (amountMaxFilter) { + const maxAmount = parseFloat(amountMaxFilter); + if (!isNaN(maxAmount) && amount > maxAmount) { + return false; + } + } + + return true; + }); + }, [grouped, submitterFilter, statusFilter, dateFromFilter, dateToFilter, amountMinFilter, amountMaxFilter]); + + // Get unique submitters and statuses for filter dropdowns + const uniqueSubmitters = useMemo(() => { + const submitters = new Set(); + grouped.forEach(({ request }) => { + const name = `${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email; + if (name) submitters.add(name); + }); + return Array.from(submitters).sort(); + }, [grouped]); + + const uniqueStatuses = useMemo(() => { + const statuses = new Set(); + grouped.forEach(({ request }) => { + const status = request.reimbursementStatuses?.[0]?.type; + if (status) statuses.add(status); + }); + return Array.from(statuses).sort(); + }, [grouped]); + + const clearFilters = () => { + setSubmitterFilter(''); + setStatusFilter(''); + setDateFromFilter(''); + setDateToFilter(''); + setAmountMinFilter(''); + setAmountMaxFilter(''); + }; + + const hasActiveFilters = + submitterFilter || statusFilter || dateFromFilter || dateToFilter || amountMinFilter || amountMaxFilter; + + const isLoading = materialsLoading || rrLoading || projectLoading; + const isError = materialsError || rrError; if (isLoading) return Loading spending history...; if (isError) return Failed to load spending history.; @@ -51,74 +190,203 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum return ( - - Spending History - - - - - - - Submitter - Date - Status - Total Amount - - - - {grouped.map(({ request, materials }) => ( - - - - handleToggleRow(request.reimbursementRequestId)}> - {openRows[request.reimbursementRequestId] ? : } - - - {request.recipient?.name || request.recipient?.email || 'N/A'} - {new Date(request.dateCreated).toLocaleDateString()} - - - - ${request.totalCost?.toFixed(2) || '0.00'} - - - - - - - Line Items - -
- - - Name - Notes - Amount - - - - {materials.map((mat) => ( - - {mat.name} - {mat.notes || '-'} - ${mat.price?.toFixed(2) || '0.00'} - - ))} - -
-
- - - - - ))} - - - + + + Spending History + + + {hasActiveFilters && ( + + {filteredData.length} of {grouped.length} results + + )} + + {hasActiveFilters && ( + + )} + + + + {showFilters && ( + + + Filter Options + + + + + Submitter + + + + + + Status + + + + + setDateFromFilter(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + + setDateToFilter(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + + setAmountMinFilter(e.target.value)} + inputProps={{ step: '0.01', min: '0' }} + /> + + + setAmountMaxFilter(e.target.value)} + inputProps={{ step: '0.01', min: '0' }} + /> + + + + )} + + {filteredData.length === 0 && grouped.length > 0 ? ( + + No spending history matches the current filters. + + + ) : ( + + + + + + Submitter / RR Link + Date + Status + Total Amount + + + + {filteredData.map(({ request, materials }) => { + return ( + + + + handleToggleRow(request.reimbursementRequestId)}> + {openRows[request.reimbursementRequestId] ? : } + + + + + {`${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email || 'N/A'} + + + {new Date(request.dateCreated).toLocaleDateString()} + + + + ${(request.totalCost / 100)?.toFixed(2) || '0.00'} + + + + + + + Line Items + + {materials.length > 0 ? ( +
+ + + Name + Notes + Amount + + + + {materials.map((mat) => ( + + {mat.name} + {mat.notes || '-'} + ${(mat.subtotal / 100)?.toFixed(2) || '0.00'} + + ))} + +
+ ) : ( + + This reimbursement request has no associated BOM line items. + It may have been created independently or with non-BOM products. + + )} + + + + + + ); + })} + + +
+ )} ); }; From 0e5d66f87d70eeb70de846ae29a70cabc5295a63 Mon Sep 17 00:00:00 2001 From: harish Date: Mon, 6 Oct 2025 18:17:10 -0400 Subject: [PATCH 4/8] only considering reimbursement requests --- .../ProjectPage/ProjectSpendingHistory.tsx | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index 3f4b478c07..afe599b553 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -28,7 +28,7 @@ import ClearIcon from '@mui/icons-material/Clear'; import { useGetMaterialsForWbsElement } from '../../hooks/bom.hooks'; import { useAllReimbursementRequests } from '../../hooks/finance.hooks'; import { useSingleProject } from '../../hooks/projects.hooks'; -import { Material, WbsNumber, ReimbursementRequest, WBSElementData, OtherProductReason, equalsWbsNumber } from 'shared'; +import { Material, WbsNumber, ReimbursementRequest, WBSElementData, equalsWbsNumber } from 'shared'; interface ProjectSpendingHistoryProps { wbsNum: WbsNumber; @@ -36,7 +36,13 @@ interface ProjectSpendingHistoryProps { const ProjectSpendingHistory: React.FC = ({ wbsNum }) => { const { data: materials, isLoading: materialsLoading, isError: materialsError } = useGetMaterialsForWbsElement(wbsNum); - const { data: allReimbursementRequests, isLoading: rrLoading, isError: rrError } = useAllReimbursementRequests(); + const { + data: allReimbursementRequests, + isLoading: rrLoading, + isError: rrError, + error: rrErrorDetails + } = useAllReimbursementRequests(); + const { data: project, isLoading: projectLoading } = useSingleProject(wbsNum); const [openRows, setOpenRows] = useState>({}); const [showFilters, setShowFilters] = useState(false); @@ -50,27 +56,13 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum const [amountMaxFilter, setAmountMaxFilter] = useState(''); const grouped = useMemo(() => { + // Return empty array if any required data is missing if (!allReimbursementRequests || !project) return []; - - // Create a map of reimbursement requests that include both: - // 1. Requests with BOM materials for this project - // 2. Requests with products linked directly to this project + + // Create a map of reimbursement requests that are linked to this project const requestMap = new Map(); - - // First, add reimbursement requests from BOM materials - if (materials) { - materials.forEach((mat) => { - const rr = mat.reimbursementRequest; - if (rr) { - if (!requestMap.has(rr.reimbursementRequestId)) { - requestMap.set(rr.reimbursementRequestId, { request: rr, materials: [] }); - } - requestMap.get(rr.reimbursementRequestId)!.materials.push(mat); - } - }); - } - - // Then, add standalone reimbursement requests linked to this project + + // First, find all reimbursement requests that are directly linked to this project allReimbursementRequests.forEach((rr) => { const hasProjectProduct = rr.reimbursementProducts.some((product) => { const reason = product.reimbursementProductReason; @@ -83,12 +75,23 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum } return false; }); - - if (hasProjectProduct && !requestMap.has(rr.reimbursementRequestId)) { + + if (hasProjectProduct) { requestMap.set(rr.reimbursementRequestId, { request: rr, materials: [] }); } }); - + + // Then, add BOM materials ONLY for reimbursement requests that are already linked to this project + if (materials && materials.length > 0) { + materials.forEach((mat) => { + const rr = mat.reimbursementRequest; + if (rr && requestMap.has(rr.reimbursementRequestId)) { + // Only add the material if the RR is already linked to this project + requestMap.get(rr.reimbursementRequestId)!.materials.push(mat); + } + }); + } + return Array.from(requestMap.values()); }, [materials, allReimbursementRequests, project, wbsNum]); @@ -97,7 +100,8 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum return grouped.filter(({ request }) => { // Submitter filter if (submitterFilter) { - const submitterName = `${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email || ''; + const submitterName = + `${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email || ''; if (!submitterName.toLowerCase().includes(submitterFilter.toLowerCase())) { return false; } @@ -178,10 +182,21 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum submitterFilter || statusFilter || dateFromFilter || dateToFilter || amountMinFilter || amountMaxFilter; const isLoading = materialsLoading || rrLoading || projectLoading; - const isError = materialsError || rrError; if (isLoading) return Loading spending history...; - if (isError) return Failed to load spending history.; + + // Handle specific errors + if (rrError) { + console.error('Failed to load reimbursement requests:', rrErrorDetails); + return Failed to load spending history.; + } + + if (materialsError) { + console.error('Failed to load materials for project'); + return Failed to load spending history.; + } + + // If we have no data but no errors, show "no spending history" if (!grouped.length) return No spending history for this project.; const handleToggleRow = (id: string) => { @@ -326,12 +341,14 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum - - {`${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email || 'N/A'} + {`${request.recipient?.firstName} ${request.recipient?.lastName}` || + request.recipient?.email || + 'N/A'} {new Date(request.dateCreated).toLocaleDateString()} @@ -372,8 +389,8 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum ) : ( - This reimbursement request has no associated BOM line items. - It may have been created independently or with non-BOM products. + This reimbursement request has no associated BOM line items. It may have been created + independently or with non-BOM products. )} From 309ee762071facdc4e3cbc87c6c9fc875801dbed Mon Sep 17 00:00:00 2001 From: harish Date: Mon, 6 Oct 2025 18:28:51 -0400 Subject: [PATCH 5/8] linting --- .../FinanceComponents/PieChart.tsx | 1 - .../ProjectViewContainer/ProjectDetails.tsx | 1 - .../ProjectPage/ProjectSpendingHistory.tsx | 23 ++----------------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx b/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx index 2020650785..5376dacc44 100644 --- a/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx +++ b/src/frontend/src/pages/FinancePage/FinanceComponents/PieChart.tsx @@ -25,7 +25,6 @@ const FinancePieChart: React.FC = ({ }) => { const [isLegendOpen, setIsLegendOpen] = useState(true); - // Combine pending categories into one const pendingReimbursement = pendingLeadership + pendingFinance + submittedToSABO; const [sectionStates, setSectionStates] = useState([ diff --git a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx index 70102f142d..5cef1c4416 100644 --- a/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx +++ b/src/frontend/src/pages/ProjectDetailPage/ProjectViewContainer/ProjectDetails.tsx @@ -20,7 +20,6 @@ import LoadingIndicator from '../../../components/LoadingIndicator'; import PieChart from '../../FinancePage/FinanceComponents/PieChart'; import WarningBanner from '../../../components/WarningBanner'; import { Box } from '@mui/system'; -import ProjectSpendingHistory from '../../ProjectPage/ProjectSpendingHistory'; export const getProjectTeamsName = (project: { teams: { teamName: string }[] }): string => { return project.teams.map((team) => team.teamName).join(', '); diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index afe599b553..9674f60a32 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -47,7 +47,6 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum const [openRows, setOpenRows] = useState>({}); const [showFilters, setShowFilters] = useState(false); - // Filter states const [submitterFilter, setSubmitterFilter] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [dateFromFilter, setDateFromFilter] = useState(''); @@ -56,22 +55,15 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum const [amountMaxFilter, setAmountMaxFilter] = useState(''); const grouped = useMemo(() => { - // Return empty array if any required data is missing if (!allReimbursementRequests || !project) return []; - // Create a map of reimbursement requests that are linked to this project const requestMap = new Map(); - // First, find all reimbursement requests that are directly linked to this project allReimbursementRequests.forEach((rr) => { const hasProjectProduct = rr.reimbursementProducts.some((product) => { const reason = product.reimbursementProductReason; - // Check if it's a WBS element and matches our project if ((reason as WBSElementData).wbsNum) { - return equalsWbsNumber( - { ...(reason as WBSElementData).wbsNum, workPackageNumber: 0 }, // Convert to project WBS - wbsNum - ); + return equalsWbsNumber((reason as WBSElementData).wbsNum, { ...wbsNum, workPackageNumber: 0 }); } return false; }); @@ -81,12 +73,10 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum } }); - // Then, add BOM materials ONLY for reimbursement requests that are already linked to this project if (materials && materials.length > 0) { materials.forEach((mat) => { const rr = mat.reimbursementRequest; if (rr && requestMap.has(rr.reimbursementRequestId)) { - // Only add the material if the RR is already linked to this project requestMap.get(rr.reimbursementRequestId)!.materials.push(mat); } }); @@ -95,10 +85,8 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum return Array.from(requestMap.values()); }, [materials, allReimbursementRequests, project, wbsNum]); - // Filter the grouped data const filteredData = useMemo(() => { return grouped.filter(({ request }) => { - // Submitter filter if (submitterFilter) { const submitterName = `${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email || ''; @@ -107,15 +95,12 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum } } - // Status filter if (statusFilter) { const currentStatus = request.reimbursementStatuses?.[0]?.type || ''; if (currentStatus !== statusFilter) { return false; } } - - // Date range filter const requestDate = new Date(request.dateCreated); if (dateFromFilter) { const fromDate = new Date(dateFromFilter); @@ -125,13 +110,12 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum } if (dateToFilter) { const toDate = new Date(dateToFilter); - toDate.setHours(23, 59, 59, 999); // End of day + toDate.setHours(23, 59, 59, 999); if (requestDate > toDate) { return false; } } - // Amount range filter const amount = (request.totalCost || 0) / 100; if (amountMinFilter) { const minAmount = parseFloat(amountMinFilter); @@ -150,7 +134,6 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum }); }, [grouped, submitterFilter, statusFilter, dateFromFilter, dateToFilter, amountMinFilter, amountMaxFilter]); - // Get unique submitters and statuses for filter dropdowns const uniqueSubmitters = useMemo(() => { const submitters = new Set(); grouped.forEach(({ request }) => { @@ -185,7 +168,6 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum if (isLoading) return Loading spending history...; - // Handle specific errors if (rrError) { console.error('Failed to load reimbursement requests:', rrErrorDetails); return Failed to load spending history.; @@ -196,7 +178,6 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum return Failed to load spending history.; } - // If we have no data but no errors, show "no spending history" if (!grouped.length) return No spending history for this project.; const handleToggleRow = (id: string) => { From 2b974326f910c4ca6dbfd7d711bdb1ee70bbe3e0 Mon Sep 17 00:00:00 2001 From: harish Date: Wed, 29 Oct 2025 18:18:17 -0400 Subject: [PATCH 6/8] progress --- .../ProjectPage/ProjectSpendingHistory.tsx | 153 ++++++++++++++---- 1 file changed, 126 insertions(+), 27 deletions(-) diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index 9674f60a32..52da4537fb 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -19,7 +19,10 @@ import { InputLabel, Select, Button, - Link + Link, + LinearProgress, + Card, + CardContent } from '@mui/material'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; @@ -101,14 +104,18 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum return false; } } - const requestDate = new Date(request.dateCreated); - if (dateFromFilter) { + const requestDate = + request.reimbursementStatuses && request.reimbursementStatuses.length > 0 + ? new Date(Math.min(...request.reimbursementStatuses.map((status) => new Date(status.dateCreated).getTime()))) + : null; + + if (dateFromFilter && requestDate) { const fromDate = new Date(dateFromFilter); if (requestDate < fromDate) { return false; } } - if (dateToFilter) { + if (dateToFilter && requestDate) { const toDate = new Date(dateToFilter); toDate.setHours(23, 59, 59, 999); if (requestDate > toDate) { @@ -161,6 +168,22 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum setAmountMaxFilter(''); }; + const budgetInfo = useMemo(() => { + if (!project || !grouped.length) return null; + + const totalBudget = project.budget; // Budget is in cents + const totalSpent = grouped.reduce((sum, { request }) => sum + (request.totalCost || 0), 0); // Total cost is in cents + const budgetRemaining = totalBudget - totalSpent; + const budgetUsedPercentage = totalBudget > 0 ? (totalSpent / totalBudget) * 100 : 0; + + return { + totalBudget: totalBudget / 100, // Convert to dollars + totalSpent: totalSpent / 100, // Convert to dollars + budgetRemaining: budgetRemaining / 100, // Convert to dollars + budgetUsedPercentage: Math.min(budgetUsedPercentage, 100) // Cap at 100% + }; + }, [project, grouped]); + const hasActiveFilters = submitterFilter || statusFilter || dateFromFilter || dateToFilter || amountMinFilter || amountMaxFilter; @@ -212,6 +235,67 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum + {budgetInfo && ( + + + + + + Budget Overview + + + + + Spent: ${budgetInfo.totalSpent.toFixed(2)} + + + Total Budget: ${budgetInfo.totalBudget.toFixed(2)} + + + 90 + ? '#f44336' + : budgetInfo.budgetUsedPercentage > 75 + ? '#ff9800' + : '#4caf50' + } + }} + /> + + + + + + Budget Remaining + + = 0 ? '#4caf50' : '#f44336', + fontWeight: 'bold' + }} + > + ${budgetInfo.budgetRemaining.toFixed(2)} + + + ({budgetInfo.budgetUsedPercentage.toFixed(1)}% used) + + + + + + + )} + {showFilters && ( @@ -306,20 +390,24 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum Submitter / RR Link - Date + Description + Date Submitted Status Total Amount {filteredData.map(({ request, materials }) => { + const hasMaterials = materials.length > 0; return ( - handleToggleRow(request.reimbursementRequestId)}> - {openRows[request.reimbursementRequestId] ? : } - + {hasMaterials ? ( + handleToggleRow(request.reimbursementRequestId)}> + {openRows[request.reimbursementRequestId] ? : } + + ) : null} = ({ wbsNum 'N/A'} - {new Date(request.dateCreated).toLocaleDateString()} + + + {request.accountCode?.name || + request.reimbursementProducts?.map((p) => p.name).join(', ') || + request.vendor?.name || + 'No description available'} + + + + {request.reimbursementStatuses && request.reimbursementStatuses.length > 0 + ? new Date( + Math.min( + ...request.reimbursementStatuses.map((status) => new Date(status.dateCreated).getTime()) + ) + ).toLocaleDateString() + : ''} + = ({ wbsNum ${(request.totalCost / 100)?.toFixed(2) || '0.00'} - - - - - - Line Items - - {materials.length > 0 ? ( + {hasMaterials && ( + + + + + + Line Items + @@ -368,16 +472,11 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum ))}
- ) : ( - - This reimbursement request has no associated BOM line items. It may have been created - independently or with non-BOM products. - - )} -
-
-
-
+
+
+
+
+ )}
); })} From cf2047d9f59e78aa77179970d50861f641d1b768 Mon Sep 17 00:00:00 2001 From: harish Date: Fri, 19 Dec 2025 10:28:02 -0500 Subject: [PATCH 7/8] #3604 finishing touches --- .../ProjectPage/ProjectSpendingHistory.tsx | 514 +++++------------- 1 file changed, 129 insertions(+), 385 deletions(-) diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index 52da4537fb..9a8b3d219b 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -1,68 +1,23 @@ -import React, { useState, useMemo } from 'react'; -import { - Box, - Typography, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - Chip, - Collapse, - IconButton, - TextField, - Grid, - MenuItem, - FormControl, - InputLabel, - Select, - Button, - Link, - LinearProgress, - Card, - CardContent -} from '@mui/material'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import FilterListIcon from '@mui/icons-material/FilterList'; -import ClearIcon from '@mui/icons-material/Clear'; -import { useGetMaterialsForWbsElement } from '../../hooks/bom.hooks'; +import React, { useMemo } from 'react'; +import { Box, Typography, Link, LinearProgress, Card, CardContent, Grid, Chip } from '@mui/material'; +import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { useAllReimbursementRequests } from '../../hooks/finance.hooks'; import { useSingleProject } from '../../hooks/projects.hooks'; -import { Material, WbsNumber, ReimbursementRequest, WBSElementData, equalsWbsNumber } from 'shared'; +import { WbsNumber, ReimbursementRequest, WBSElementData, equalsWbsNumber } from 'shared'; +import LoadingIndicator from '../../components/LoadingIndicator'; interface ProjectSpendingHistoryProps { wbsNum: WbsNumber; } const ProjectSpendingHistory: React.FC = ({ wbsNum }) => { - const { data: materials, isLoading: materialsLoading, isError: materialsError } = useGetMaterialsForWbsElement(wbsNum); - const { - data: allReimbursementRequests, - isLoading: rrLoading, - isError: rrError, - error: rrErrorDetails - } = useAllReimbursementRequests(); - + const { data: allReimbursementRequests, isLoading: rrLoading, isError: rrError } = useAllReimbursementRequests(); const { data: project, isLoading: projectLoading } = useSingleProject(wbsNum); - const [openRows, setOpenRows] = useState>({}); - const [showFilters, setShowFilters] = useState(false); - - const [submitterFilter, setSubmitterFilter] = useState(''); - const [statusFilter, setStatusFilter] = useState(''); - const [dateFromFilter, setDateFromFilter] = useState(''); - const [dateToFilter, setDateToFilter] = useState(''); - const [amountMinFilter, setAmountMinFilter] = useState(''); - const [amountMaxFilter, setAmountMaxFilter] = useState(''); - const grouped = useMemo(() => { + const reimbursementRequests = useMemo(() => { if (!allReimbursementRequests || !project) return []; - const requestMap = new Map(); - - allReimbursementRequests.forEach((rr) => { + return allReimbursementRequests.filter((rr) => { const hasProjectProduct = rr.reimbursementProducts.some((product) => { const reason = product.reimbursementProductReason; if ((reason as WBSElementData).wbsNum) { @@ -70,170 +25,120 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum } return false; }); - - if (hasProjectProduct) { - requestMap.set(rr.reimbursementRequestId, { request: rr, materials: [] }); - } + return hasProjectProduct; }); + }, [allReimbursementRequests, project, wbsNum]); - if (materials && materials.length > 0) { - materials.forEach((mat) => { - const rr = mat.reimbursementRequest; - if (rr && requestMap.has(rr.reimbursementRequestId)) { - requestMap.get(rr.reimbursementRequestId)!.materials.push(mat); - } - }); + const getSubmittedDate = (rr: ReimbursementRequest): Date | null => { + const pendingFinanceStatus = rr.reimbursementStatuses?.find((status) => status.type === 'PENDING_FINANCE'); + if (pendingFinanceStatus) { + return new Date(pendingFinanceStatus.dateCreated); } - - return Array.from(requestMap.values()); - }, [materials, allReimbursementRequests, project, wbsNum]); - - const filteredData = useMemo(() => { - return grouped.filter(({ request }) => { - if (submitterFilter) { - const submitterName = - `${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email || ''; - if (!submitterName.toLowerCase().includes(submitterFilter.toLowerCase())) { - return false; - } - } - - if (statusFilter) { - const currentStatus = request.reimbursementStatuses?.[0]?.type || ''; - if (currentStatus !== statusFilter) { - return false; - } - } - const requestDate = - request.reimbursementStatuses && request.reimbursementStatuses.length > 0 - ? new Date(Math.min(...request.reimbursementStatuses.map((status) => new Date(status.dateCreated).getTime()))) - : null; - - if (dateFromFilter && requestDate) { - const fromDate = new Date(dateFromFilter); - if (requestDate < fromDate) { - return false; - } - } - if (dateToFilter && requestDate) { - const toDate = new Date(dateToFilter); - toDate.setHours(23, 59, 59, 999); - if (requestDate > toDate) { - return false; - } - } - - const amount = (request.totalCost || 0) / 100; - if (amountMinFilter) { - const minAmount = parseFloat(amountMinFilter); - if (!isNaN(minAmount) && amount < minAmount) { - return false; - } - } - if (amountMaxFilter) { - const maxAmount = parseFloat(amountMaxFilter); - if (!isNaN(maxAmount) && amount > maxAmount) { - return false; - } - } - - return true; - }); - }, [grouped, submitterFilter, statusFilter, dateFromFilter, dateToFilter, amountMinFilter, amountMaxFilter]); - - const uniqueSubmitters = useMemo(() => { - const submitters = new Set(); - grouped.forEach(({ request }) => { - const name = `${request.recipient?.firstName} ${request.recipient?.lastName}` || request.recipient?.email; - if (name) submitters.add(name); - }); - return Array.from(submitters).sort(); - }, [grouped]); - - const uniqueStatuses = useMemo(() => { - const statuses = new Set(); - grouped.forEach(({ request }) => { - const status = request.reimbursementStatuses?.[0]?.type; - if (status) statuses.add(status); - }); - return Array.from(statuses).sort(); - }, [grouped]); - - const clearFilters = () => { - setSubmitterFilter(''); - setStatusFilter(''); - setDateFromFilter(''); - setDateToFilter(''); - setAmountMinFilter(''); - setAmountMaxFilter(''); + return null; }; + const rows = useMemo(() => { + return reimbursementRequests.map((rr) => ({ + id: rr.reimbursementRequestId, + submitter: `${rr.recipient?.firstName} ${rr.recipient?.lastName}` || rr.recipient?.email || 'N/A', + description: + rr.reimbursementProducts?.map((p) => p.name).join(', ') || + rr.accountCode?.name || + rr.vendor?.name || + 'No description', + dateSubmitted: getSubmittedDate(rr), + status: rr.reimbursementStatuses?.[0]?.type || 'UNKNOWN', + totalAmount: (rr.totalCost || 0) / 100, + reimbursementRequestId: rr.reimbursementRequestId + })); + }, [reimbursementRequests]); + const budgetInfo = useMemo(() => { - if (!project || !grouped.length) return null; + if (!project) return null; - const totalBudget = project.budget; // Budget is in cents - const totalSpent = grouped.reduce((sum, { request }) => sum + (request.totalCost || 0), 0); // Total cost is in cents + const totalBudget = project.budget; + const totalSpent = reimbursementRequests.reduce((sum, rr) => sum + (rr.totalCost || 0), 0); const budgetRemaining = totalBudget - totalSpent; const budgetUsedPercentage = totalBudget > 0 ? (totalSpent / totalBudget) * 100 : 0; return { - totalBudget: totalBudget / 100, // Convert to dollars - totalSpent: totalSpent / 100, // Convert to dollars - budgetRemaining: budgetRemaining / 100, // Convert to dollars - budgetUsedPercentage: Math.min(budgetUsedPercentage, 100) // Cap at 100% + totalBudget: totalBudget / 100, + totalSpent: totalSpent / 100, + budgetRemaining: budgetRemaining / 100, + budgetUsedPercentage: Math.min(budgetUsedPercentage, 100) }; - }, [project, grouped]); - - const hasActiveFilters = - submitterFilter || statusFilter || dateFromFilter || dateToFilter || amountMinFilter || amountMaxFilter; + }, [project, reimbursementRequests]); + + const columns: GridColDef[] = [ + { + field: 'submitter', + headerName: 'Submitter', + flex: 1, + minWidth: 150, + renderCell: (params: GridRenderCellParams) => ( + + {params.value} + + ) + }, + { + field: 'description', + headerName: 'Description', + flex: 2, + minWidth: 250 + }, + { + field: 'dateSubmitted', + headerName: 'Date Submitted', + flex: 1, + minWidth: 130, + type: 'date', + valueFormatter: (params) => { + return params.value ? new Date(params.value).toLocaleDateString() : '-'; + } + }, + { + field: 'status', + headerName: 'Status', + flex: 1, + minWidth: 150, + renderCell: (params: GridRenderCellParams) => ( + + ) + }, + { + field: 'totalAmount', + headerName: 'Total Amount', + flex: 0.7, + minWidth: 120, + type: 'number', + valueFormatter: (params) => `$${params.value.toFixed(2)}` + } + ]; - const isLoading = materialsLoading || rrLoading || projectLoading; + const isLoading = rrLoading || projectLoading; - if (isLoading) return Loading spending history...; + if (isLoading) return ; if (rrError) { - console.error('Failed to load reimbursement requests:', rrErrorDetails); - return Failed to load spending history.; - } - - if (materialsError) { - console.error('Failed to load materials for project'); return Failed to load spending history.; } - if (!grouped.length) return No spending history for this project.; - - const handleToggleRow = (id: string) => { - setOpenRows((prev) => ({ ...prev, [id]: !prev[id] })); - }; + if (!reimbursementRequests.length) return No spending history for this project.; return ( - - - Spending History - - - {hasActiveFilters && ( - - {filteredData.length} of {grouped.length} results - - )} - - {hasActiveFilters && ( - - )} - - + + Spending History + {budgetInfo && ( @@ -256,7 +161,7 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum variant="determinate" value={budgetInfo.budgetUsedPercentage} sx={{ - height: 8, + height: 10, borderRadius: 5, backgroundColor: '#444', '& .MuiLinearProgress-bar': { @@ -278,7 +183,7 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum Budget Remaining
= 0 ? '#4caf50' : '#f44336', fontWeight: 'bold' @@ -296,194 +201,33 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum )} - {showFilters && ( - - - Filter Options - - - - - Submitter - - - - - - Status - - - - - setDateFromFilter(e.target.value)} - InputLabelProps={{ shrink: true }} - /> - - - setDateToFilter(e.target.value)} - InputLabelProps={{ shrink: true }} - /> - - - setAmountMinFilter(e.target.value)} - inputProps={{ step: '0.01', min: '0' }} - /> - - - setAmountMaxFilter(e.target.value)} - inputProps={{ step: '0.01', min: '0' }} - /> - - - - )} - - {filteredData.length === 0 && grouped.length > 0 ? ( - - No spending history matches the current filters. - - - ) : ( - - - - - - Submitter / RR Link - Description - Date Submitted - Status - Total Amount - - - - {filteredData.map(({ request, materials }) => { - const hasMaterials = materials.length > 0; - return ( - - - - {hasMaterials ? ( - handleToggleRow(request.reimbursementRequestId)}> - {openRows[request.reimbursementRequestId] ? : } - - ) : null} - - - - {`${request.recipient?.firstName} ${request.recipient?.lastName}` || - request.recipient?.email || - 'N/A'} - - - - - {request.accountCode?.name || - request.reimbursementProducts?.map((p) => p.name).join(', ') || - request.vendor?.name || - 'No description available'} - - - - {request.reimbursementStatuses && request.reimbursementStatuses.length > 0 - ? new Date( - Math.min( - ...request.reimbursementStatuses.map((status) => new Date(status.dateCreated).getTime()) - ) - ).toLocaleDateString() - : ''} - - - - - ${(request.totalCost / 100)?.toFixed(2) || '0.00'} - - {hasMaterials && ( - - - - - - Line Items - -
- - - Name - Notes - Amount - - - - {materials.map((mat) => ( - - {mat.name} - {mat.notes || '-'} - ${(mat.subtotal / 100)?.toFixed(2) || '0.00'} - - ))} - -
-
- - - - )} - - ); - })} - - - - )} + + + ); }; From c8aa259d59e5bfcb4d54368721701da62c79ff11 Mon Sep 17 00:00:00 2001 From: harish Date: Fri, 19 Dec 2025 10:29:55 -0500 Subject: [PATCH 8/8] #3604 prettier --- .../src/pages/ProjectPage/ProjectSpendingHistory.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx index 9a8b3d219b..641253f9f2 100644 --- a/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx +++ b/src/frontend/src/pages/ProjectPage/ProjectSpendingHistory.tsx @@ -56,16 +56,16 @@ const ProjectSpendingHistory: React.FC = ({ wbsNum const budgetInfo = useMemo(() => { if (!project) return null; - const totalBudget = project.budget; - const totalSpent = reimbursementRequests.reduce((sum, rr) => sum + (rr.totalCost || 0), 0); + const totalBudget = project.budget; + const totalSpent = reimbursementRequests.reduce((sum, rr) => sum + (rr.totalCost || 0), 0); const budgetRemaining = totalBudget - totalSpent; const budgetUsedPercentage = totalBudget > 0 ? (totalSpent / totalBudget) * 100 : 0; return { - totalBudget: totalBudget / 100, - totalSpent: totalSpent / 100, - budgetRemaining: budgetRemaining / 100, - budgetUsedPercentage: Math.min(budgetUsedPercentage, 100) + totalBudget: totalBudget / 100, + totalSpent: totalSpent / 100, + budgetRemaining: budgetRemaining / 100, + budgetUsedPercentage: Math.min(budgetUsedPercentage, 100) }; }, [project, reimbursementRequests]);