From 43b8cac9602f03def3f8fac7f9a1a8eab82b5934 Mon Sep 17 00:00:00 2001 From: Vinay Vk Date: Fri, 15 May 2026 18:53:15 -0500 Subject: [PATCH 1/4] Enchanced Planned vs Actual Cost Visibility with variance and budget indicators --- .../ActualVsPlannedCost.jsx | 99 ++++++++++++-- .../ActualVsPlannedCost.module.css | 128 ++++++++++++++++++ 2 files changed, 219 insertions(+), 8 deletions(-) diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx index b2e139f3f0..3472ce0f83 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx @@ -105,6 +105,28 @@ function ActualVsPlannedCost() { const filterSummary = `${selectedProjectName || 'Loading...'} - ${selectedCategory}`; + const chartDataWithVariance = chartData.map(item => { + const variance = item.actualCost - item.plannedCost; + return { + ...item, + variance, + variancePct: item.plannedCost > 0 ? (variance / item.plannedCost) * 100 : null, + budgetStatus: variance > 0 ? 'Over Budget' : variance < 0 ? 'Under Budget' : 'On Budget', + }; + }); + + const hasData = + chartDataWithVariance.length > 0 && + !( + chartDataWithVariance.length === 1 && + chartDataWithVariance[0].actualCost === 0 && + chartDataWithVariance[0].plannedCost === 0 + ); + + const totalVariance = totals.actual - totals.planned; + const totalVariancePct = totals.planned > 0 ? (totalVariance / totals.planned) * 100 : null; + const isTotalOverrun = totalVariance > 0; + let chartContent; if (loading || isFiltering) { chartContent = ( @@ -121,10 +143,7 @@ function ActualVsPlannedCost() { Updating chart... ); - } else if ( - !chartData.length || - (chartData.length === 1 && chartData[0].actualCost === 0 && chartData[0].plannedCost === 0) - ) { + } else if (!hasData) { chartContent = (
- + +

Actual vs Planned Costs @@ -234,6 +263,60 @@ function ActualVsPlannedCost() {

{chartContent} + + {!loading && !isFiltering && hasData && ( +
+
+

Variance and Budget Indicators

+
+ Total Variance: {isTotalOverrun ? '+' : ''} + {totalVariance.toLocaleString()} + {totalVariancePct !== null && + ` (${isTotalOverrun ? '+' : ''}${totalVariancePct.toFixed(1)}%)`} +
+
+ +
+ {chartDataWithVariance.map(item => { + const isOverrun = item.variance > 0; + const isUnderBudget = item.variance < 0; + const cardClass = isOverrun + ? styles.varianceOverrun + : isUnderBudget + ? styles.varianceUnder + : styles.varianceNeutral; + + return ( +
+
{item.category}
+
+ Planned: + {item.plannedCost.toLocaleString()} +
+
+ Actual: + {item.actualCost.toLocaleString()} +
+
+ Variance: + + {isOverrun ? '+' : ''} + {item.variance.toLocaleString()} + +
+ {item.variancePct !== null && ( +
+ {isOverrun ? '+' : ''} + {item.variancePct.toFixed(1)}% +
+ )} +
{item.budgetStatus}
+
+ ); + })} +
+
+ )}
); } diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.module.css b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.module.css index 6d1152ac71..053248bb46 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.module.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.module.css @@ -43,6 +43,134 @@ color: var(--text-color); } +.varianceSummaryContainer { + margin-top: 12px; + border-top: 1px solid var(--button-hover); + padding-top: 10px; +} + +.varianceSummaryHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; + flex-wrap: wrap; +} + +.varianceSummaryTitle { + font-size: 0.9rem; + margin: 0; + color: var(--text-color); +} + +.totalOverrunBadge, +.totalOnTrackBadge { + font-size: 0.75rem; + font-weight: 700; + padding: 4px 8px; + border-radius: 999px; + border: 1px solid transparent; +} + +.totalOverrunBadge { + color: #b63d30; + background: rgba(231, 74, 59, 0.14); + border-color: rgba(231, 74, 59, 0.35); +} + +.totalOnTrackBadge { + color: #0f7f5b; + background: rgba(28, 200, 138, 0.14); + border-color: rgba(28, 200, 138, 0.35); +} + +.varianceCardsRow { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + gap: 10px; +} + +.varianceCard { + border-radius: 8px; + border: 1px solid transparent; + padding: 10px; +} + +.varianceOverrun { + background: rgba(231, 74, 59, 0.1); + border-color: rgba(231, 74, 59, 0.35); +} + +.varianceUnder { + background: rgba(28, 200, 138, 0.1); + border-color: rgba(28, 200, 138, 0.35); +} + +.varianceNeutral { + background: rgba(128, 128, 128, 0.08); + border-color: rgba(128, 128, 128, 0.2); +} + +.varianceCardCategory { + font-size: 0.82rem; + font-weight: 700; + color: var(--text-color); + margin-bottom: 8px; +} + +.varianceCardRow { + display: flex; + align-items: center; + justify-content: space-between; + color: var(--text-color); + font-size: 0.8rem; + margin-bottom: 3px; +} + +.varianceCardPct { + margin-top: 4px; + font-size: 0.78rem; + font-weight: 700; + color: var(--text-color); +} + +.varianceCardStatus { + margin-top: 6px; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.02em; + color: var(--text-color); +} + +.darkMode .totalOverrunBadge { + color: #ffb7af; + background: rgba(231, 74, 59, 0.2); + border-color: rgba(255, 183, 175, 0.45); +} + +.darkMode .totalOnTrackBadge { + color: #9ce7cb; + background: rgba(28, 200, 138, 0.2); + border-color: rgba(156, 231, 203, 0.45); +} + +.darkMode .varianceOverrun { + background: rgba(231, 74, 59, 0.18); + border-color: rgba(255, 183, 175, 0.3); +} + +.darkMode .varianceUnder { + background: rgba(28, 200, 138, 0.18); + border-color: rgba(156, 231, 203, 0.3); +} + +.darkMode .varianceNeutral { + background: rgba(160, 170, 190, 0.12); + border-color: rgba(160, 170, 190, 0.28); +} + @media (width <= 768px) { .selectorsContainer { flex-direction: column; From e7a0e87076975d23d5ab2f5014044847a1c92d79 Mon Sep 17 00:00:00 2001 From: Vinay Vk Date: Sun, 31 May 2026 00:39:21 -0500 Subject: [PATCH 2/4] Fix SonarCloud issues, double-slash API URL, wire expenditure cards, fix vitest peer dep --- package.json | 2 +- .../ActualVsPlannedCost.jsx | 52 +++++++++++-------- .../WeeklyProjectSummary.jsx | 6 ++- src/services/projectCostService.js | 4 +- 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 937b1789af..46fb4db483 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "@typescript-eslint/eslint-plugin": "^8.44.1", "@typescript-eslint/parser": "^8.44.1", "@vitejs/plugin-react": "^4.5.0", - "@vitest/ui": "3.2.2", + "@vitest/ui": "^3.2.0", "babel-jest": "^29.7.0", "baseline-browser-mapping": "^2.9.17", "cross-env": "^5.2.1", diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx index 3472ce0f83..a1e54b0720 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx @@ -17,6 +17,18 @@ import { fetchBMProjects } from '../../../../actions/bmdashboard/projectActions' import { ENDPOINTS } from '../../../../utils/URL'; import styles from './ActualVsPlannedCost.module.css'; +function getBudgetStatus(variance) { + if (variance > 0) return 'Over Budget'; + if (variance < 0) return 'Under Budget'; + return 'On Budget'; +} + +function getVarianceCardClass(variance, cardStyles) { + if (variance > 0) return cardStyles.varianceOverrun; + if (variance < 0) return cardStyles.varianceUnder; + return cardStyles.varianceNeutral; +} + function ActualVsPlannedCost() { const dispatch = useDispatch(); const projects = useSelector(state => state.bmProjects) || []; @@ -111,7 +123,7 @@ function ActualVsPlannedCost() { ...item, variance, variancePct: item.plannedCost > 0 ? (variance / item.plannedCost) * 100 : null, - budgetStatus: variance > 0 ? 'Over Budget' : variance < 0 ? 'Under Budget' : 'On Budget', + budgetStatus: getBudgetStatus(variance), }; }); @@ -143,22 +155,7 @@ function ActualVsPlannedCost() { Updating chart...
); - } else if (!hasData) { - chartContent = ( -
- No data available for the selected filters. -
- ); - } else { + } else if (hasData) { chartContent = (
@@ -214,6 +211,21 @@ function ActualVsPlannedCost() {
); + } else { + chartContent = ( +
+ No data available for the selected filters. +
+ ); } return ( @@ -280,11 +292,7 @@ function ActualVsPlannedCost() { {chartDataWithVariance.map(item => { const isOverrun = item.variance > 0; const isUnderBudget = item.variance < 0; - const cardClass = isOverrun - ? styles.varianceOverrun - : isUnderBudget - ? styles.varianceUnder - : styles.varianceNeutral; + const cardClass = getVarianceCardClass(item.variance, styles); return (
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index b03d9926a5..a0b5136d78 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -28,6 +28,8 @@ import LossTrackingLineChart from './Financials/LossTrackingLineCharts/LossTrack import SupplierPerformanceGraph from './SupplierPerformanceGraph.jsx'; import MostFrequentKeywords from './MostFrequentKeywords/MostFrequentKeywords'; import DistributionLaborHours from './DistributionLaborHours/DistributionLaborHours'; +import FinancialsTrackingCard from './ExpenditureChart/FinancialsTrackingCard'; +import FinancialStatButtons from './Financials/FinancialStatButtons'; const projectStatusButtons = [ { @@ -397,9 +399,11 @@ function WeeklyProjectSummary() { className={`${styles.weeklyProjectSummaryCard} ${styles.normalCard}`} > {(() => { + if (index === 0) return ; + if (index === 1) return ; if (index === 2) return ; if (index === 3) return ; - return '📊 Card'; + return null; })()}
); diff --git a/src/services/projectCostService.js b/src/services/projectCostService.js index 66f0f1dfcb..bbde84c02f 100644 --- a/src/services/projectCostService.js +++ b/src/services/projectCostService.js @@ -4,11 +4,11 @@ import { ApiEndpoint } from '../utils/URL'; const ApiUri = `${ApiEndpoint}/`; const getProjectCosts = projectId => { - return httpService.get(`${ApiUri}/project/${projectId}/costs`); + return httpService.get(`${ApiUri}project/${projectId}/costs`); }; const getProjectPredictions = projectId => { - return httpService.get(`${ApiUri}/project/${projectId}/predictions`); + return httpService.get(`${ApiUri}project/${projectId}/predictions`); }; export default { From 54f9a4926b9728782b99c6f7c3f2823c53274728 Mon Sep 17 00:00:00 2001 From: Vinay Vk Date: Sun, 31 May 2026 00:48:42 -0500 Subject: [PATCH 3/4] Fix SonarCloud: extract VarianceCard component, remove unused isUnderBudget variable --- .../ActualVsPlannedCost.jsx | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx index a1e54b0720..8a61b9f943 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx @@ -29,6 +29,38 @@ function getVarianceCardClass(variance, cardStyles) { return cardStyles.varianceNeutral; } +function VarianceCard({ item, cardStyles }) { + const isOverrun = item.variance > 0; + const cardClass = getVarianceCardClass(item.variance, cardStyles); + return ( +
+
{item.category}
+
+ Planned: + {item.plannedCost.toLocaleString()} +
+
+ Actual: + {item.actualCost.toLocaleString()} +
+
+ Variance: + + {isOverrun ? '+' : ''} + {item.variance.toLocaleString()} + +
+ {item.variancePct !== null && ( +
+ {isOverrun ? '+' : ''} + {item.variancePct.toFixed(1)}% +
+ )} +
{item.budgetStatus}
+
+ ); +} + function ActualVsPlannedCost() { const dispatch = useDispatch(); const projects = useSelector(state => state.bmProjects) || []; @@ -289,39 +321,9 @@ function ActualVsPlannedCost() {
- {chartDataWithVariance.map(item => { - const isOverrun = item.variance > 0; - const isUnderBudget = item.variance < 0; - const cardClass = getVarianceCardClass(item.variance, styles); - - return ( -
-
{item.category}
-
- Planned: - {item.plannedCost.toLocaleString()} -
-
- Actual: - {item.actualCost.toLocaleString()} -
-
- Variance: - - {isOverrun ? '+' : ''} - {item.variance.toLocaleString()} - -
- {item.variancePct !== null && ( -
- {isOverrun ? '+' : ''} - {item.variancePct.toFixed(1)}% -
- )} -
{item.budgetStatus}
-
- ); - })} + {chartDataWithVariance.map(item => ( + + ))}
)} From d1680ea7310cec8e784170f5b3a6336352064f67 Mon Sep 17 00:00:00 2001 From: Vinay Vk Date: Sun, 31 May 2026 00:53:28 -0500 Subject: [PATCH 4/4] Fix SonarCloud: add PropTypes to VarianceCard, extract buildChartContent to reduce cognitive complexity --- .../ActualVsPlannedCost.jsx | 207 ++++++++++-------- 1 file changed, 119 insertions(+), 88 deletions(-) diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx index 8a61b9f943..54493fbcfe 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx @@ -1,4 +1,5 @@ import { useEffect, useState, useMemo } from 'react'; +import PropTypes from 'prop-types'; import axios from 'axios'; import { useDispatch, useSelector } from 'react-redux'; import { @@ -61,6 +62,117 @@ function VarianceCard({ item, cardStyles }) { ); } +VarianceCard.propTypes = { + item: PropTypes.shape({ + category: PropTypes.string.isRequired, + plannedCost: PropTypes.number.isRequired, + actualCost: PropTypes.number.isRequired, + variance: PropTypes.number.isRequired, + variancePct: PropTypes.number, + budgetStatus: PropTypes.string.isRequired, + }).isRequired, + cardStyles: PropTypes.shape({ + varianceCard: PropTypes.string, + varianceOverrun: PropTypes.string, + varianceUnder: PropTypes.string, + varianceNeutral: PropTypes.string, + varianceCardCategory: PropTypes.string, + varianceCardRow: PropTypes.string, + varianceCardPct: PropTypes.string, + varianceCardStatus: PropTypes.string, + }).isRequired, +}; + +function buildChartContent({ loading, isFiltering, hasData, chartDataWithVariance, darkMode }) { + if (loading || isFiltering) { + return ( +
+ + Updating chart... +
+ ); + } + if (hasData) { + return ( +
+ + + + + + + + + + + + + + + +
+ ); + } + return ( +
+ No data available for the selected filters. +
+ ); +} + function ActualVsPlannedCost() { const dispatch = useDispatch(); const projects = useSelector(state => state.bmProjects) || []; @@ -171,94 +283,13 @@ function ActualVsPlannedCost() { const totalVariancePct = totals.planned > 0 ? (totalVariance / totals.planned) * 100 : null; const isTotalOverrun = totalVariance > 0; - let chartContent; - if (loading || isFiltering) { - chartContent = ( -
- - Updating chart... -
- ); - } else if (hasData) { - chartContent = ( -
- - - - - - - - - - - - - - - -
- ); - } else { - chartContent = ( -
- No data available for the selected filters. -
- ); - } + const chartContent = buildChartContent({ + loading, + isFiltering, + hasData, + chartDataWithVariance, + darkMode, + }); return (