diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx
index b2e139f3f0..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 {
@@ -17,6 +18,161 @@ 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 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}
+
+ );
+}
+
+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 (
+
+
Actual vs Planned Costs
@@ -234,6 +338,26 @@ function ActualVsPlannedCost() {
{chartContent}
+
+ {!loading && !isFiltering && hasData && (
+
+
+
Variance and Budget Indicators
+
+ Total Variance: {isTotalOverrun ? '+' : ''}
+ {totalVariance.toLocaleString()}
+ {totalVariancePct !== null &&
+ ` (${isTotalOverrun ? '+' : ''}${totalVariancePct.toFixed(1)}%)`}
+
+
+
+
+ {chartDataWithVariance.map(item => (
+
+ ))}
+
+
+ )}
);
}
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;
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx
index 314c135769..46739b2648 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx
@@ -2,6 +2,7 @@ import { useState } from 'react';
import ActualVsPlannedCost from '../ActualVsPlannedCost/ActualVsPlannedCost';
import FinancialsTrackingCard from './FinancialsTrackingCard';
import SingleExpenditureCard from './SingleExpenditureCard';
+import CostPredictionChart from '../CostPredictionChart';
import styles from './FinancialsTrackingSection.module.css';
/**
@@ -87,8 +88,8 @@ function FinancialsTrackingSection() {
-
) : (
@@ -99,8 +100,8 @@ function FinancialsTrackingSection() {
-
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 {