diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx index 1b583f57ab9..fb3d308fd95 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ActualVsPlannedCost/ActualVsPlannedCost.jsx @@ -92,7 +92,14 @@ VarianceCard.propTypes = { }).isRequired, }; -function buildChartContent({ loading, isFiltering, hasData, chartDataWithVariance, darkMode }) { +function buildChartContent({ + loading, + isFiltering, + error, + hasData, + chartDataWithVariance, + darkMode, +}) { if (loading || isFiltering) { return (
); } + if (error) { + return ( +
+ {error} +
+ ); + } if (hasData) { return (
@@ -188,7 +202,7 @@ function buildChartContent({ loading, isFiltering, hasData, chartDataWithVarianc ); } -function ActualVsPlannedCost() { +function ActualVsPlannedCost({ onProjectChange, onCategoryChange }) { const dispatch = useDispatch(); const projects = useSelector(state => state.bmProjects) || []; const darkMode = useSelector(state => state.theme.darkMode); @@ -205,6 +219,7 @@ function ActualVsPlannedCost() { const [breakdown, setBreakdown] = useState([]); const [totals, setTotals] = useState({ actual: 0, planned: 0 }); const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); const [isFiltering, setIsFiltering] = useState(false); const selectedProjectName = useMemo( @@ -227,9 +242,12 @@ function ActualVsPlannedCost() { // Default to first project if none selected useEffect(() => { if (!selectedProject && projects.length > 0) { - setSelectedProject(projects[0]._id); + const defaultProjectId = projects[0]._id; + setSelectedProject(defaultProjectId); + onProjectChange(defaultProjectId); + onCategoryChange('Overall'); } - }, [projects, selectedProject]); + }, [projects, selectedProject, onProjectChange, onCategoryChange]); // Filter transition effect useEffect(() => { @@ -243,27 +261,40 @@ function ActualVsPlannedCost() { // Fetch project expenses useEffect(() => { if (selectedProject) { + let cancelled = false; setLoading(true); + setError(null); axios .get(ENDPOINTS.BM_PROJECT_EXPENSE_BY_ID(selectedProject)) .then(({ data }) => { - setTotals({ - actual: Math.round(data.totalActualCost), - planned: Math.round(data.totalPlannedCost), - }); - setBreakdown( - data.breakdown.map(item => ({ - category: item.category, - actualCost: Math.round(item.actualCost), - plannedCost: Math.round(item.plannedCost), - })), - ); + if (!cancelled) { + setTotals({ + actual: Math.round(data.totalActualCost), + planned: Math.round(data.totalPlannedCost), + }); + setBreakdown( + data.breakdown.map(item => ({ + category: item.category, + actualCost: Math.round(item.actualCost), + plannedCost: Math.round(item.plannedCost), + })), + ); + } }) - .catch(() => { - setTotals({ actual: 0, planned: 0 }); - setBreakdown([]); + .catch(fetchError => { + if (!cancelled) { + setTotals({ actual: 0, planned: 0 }); + setBreakdown([]); + setError(fetchError?.response?.data?.message || 'Failed to load project expense data.'); + } }) - .finally(() => setLoading(false)); + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { + cancelled = true; + }; } }, [selectedProject]); @@ -305,6 +336,7 @@ function ActualVsPlannedCost() { const chartContent = buildChartContent({ loading, isFiltering, + error, hasData, chartDataWithVariance, darkMode, @@ -329,7 +361,9 @@ function ActualVsPlannedCost() { value={selectedProject} onChange={e => { setSelectedProject(e.target.value); + onProjectChange(e.target.value); setSelectedCategory('Overall'); + onCategoryChange('Overall'); }} > {projects.map(p => ( @@ -345,7 +379,10 @@ function ActualVsPlannedCost() { setSelectedProjectId(e.target.value)} - > - - {PROJECTS.map(p => ( - - ))} - -
-
- - - {/* Grid */} - - {/* Axes: tick text, tick marks, baseline lines */} - ( - - {payload.value} - - )} - tickMargin={0} // apply margin at XAxis level, not inside - /> - Math.ceil(dataMax * 1.15)]} - tick={({ x, y, payload }) => ( - - {payload.value} - - )} - /> - {/* Tooltip & Legend */} - } cursor={{ stroke: theme.accent }} /> - ( -
    - {legendItems.map(item => ( -
  • - {/* icon */} - {item.type === 'circle' ? ( - - ) : ( - - + No cost tracking data is available for the selected project and date range. +

    + ) : ( + + + {/* Grid */} + + {/* Axes: tick text, tick marks, baseline lines */} + + Math.ceil(dataMax * 1.15)]} + tick={({ x, y, payload }) => ( + + {payload.value} + + )} + /> + {/* Tooltip & Legend */} + } + labelFormatter={formatMonthLabel} + cursor={{ stroke: theme.accent }} + /> + ( +
      + {legendItems.map(item => ( +
    • + {/* icon */} + {item.type === 'circle' ? ( + - - )} - {/* label */} - {item.label} -
    • - ))} -
    - )} - /> - {/* Reference line marking the current month. Label sits near the bottom + ) : ( + + + + )} + {/* label */} + {item.label} +
  • + ))} +
+ )} + /> + {/* Reference line marking the current month. Label sits near the bottom of the line so it never overlaps the data value labels.*/} - } - /> - {/* Series (kept fixed colors) */} - - - -
-
+ } + /> + {/* Series (kept fixed colors) */} + + + + + )} ); } CostPredictionChart.propTypes = { - projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + projectId: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, }; export default CostPredictionChart; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.jsx index c65a6f751d5..51572c22637 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.jsx @@ -101,11 +101,13 @@ function ExpenditureCard({ mode = 'stacked', pieType }) { - {selectedProject && ( + {selectedProject ? ( + ) : ( +

No expenditure projects found.

)} ); diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx index e4d77783627..927886a3d74 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx @@ -13,14 +13,38 @@ import styles from './FinancialsTrackingSection.module.css'; * * Stacked (default) — 4 independent cards in a 2×2 grid: * [Actual Expenditure] [Planned Expenditure] - * [Actual vs Planned] [Placeholder] + * [Actual vs Planned] [Planned vs Actual costs tracking] * * Comparison — 3 cards (combined card spans full width at top): * [Combined Expenditure Card — shared filter, both pies S-by-S] - * [Actual vs Planned] [Placeholder] + * [Actual vs Planned] [Planned vs Actual costs tracking] */ function FinancialsTrackingSection() { const [viewMode, setViewMode] = useState('stacked'); + const [selectedProjectId, setSelectedProjectId] = useState( + () => localStorage.getItem('bm_avsp_project') || '', + ); + const [selectedCategory, setSelectedCategory] = useState( + () => localStorage.getItem('bm_avsp_category') || 'Overall', + ); + + const renderCostCharts = () => ( + <> +
+ +
+
+ {selectedProjectId ? ( + + ) : ( +

Select a project to view cost tracking.

+ )} +
+ + ); return (
@@ -85,25 +109,13 @@ function FinancialsTrackingSection() {
-
- -
-
- -
+ {renderCostCharts()}
) : ( /* ── Comparison: combined card on top, two cards below ───── */
-
-
- -
-
- -
-
+
{renderCostCharts()}
)}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index d1807fe3a48..390b5b7c88e 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -28,14 +28,12 @@ import SupplierPerformanceGraph from './SupplierPerformanceGraph.jsx'; import MostFrequentKeywords from './MostFrequentKeywords/MostFrequentKeywords'; import LessonsLearntChart from '../LessonsLearnt/LessonsLearntChart'; import DistributionLaborHours from './DistributionLaborHours/DistributionLaborHours'; -import ActualVsPlannedCost from './ActualVsPlannedCost/ActualVsPlannedCost'; import { MaterialConsumptionCards } from './MaterialConsumption/MaterialConsumption'; import ToolsStoppageHorizontalBarChart from './Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart'; import ToolStatusDonutChart from './ToolStatusDonutChart/ToolStatusDonutChart'; import InjurySeverityChart from '../Injuries/InjurySeverityChart'; -import CostPredictionChart from './CostPredictionChart'; const projectStatusButtons = [ { @@ -417,21 +415,6 @@ function WeeklyProjectSummary() { content: (
-
-
- -
-
- -
-
), }, diff --git a/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx b/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx index d5751c59432..eaa8155804d 100644 --- a/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx +++ b/src/components/CommunityPortal/Activities/__tests__/ActivityList.test.jsx @@ -58,6 +58,7 @@ describe('ActivityList', () => { test('sorts activities by date (latest to earliest)', () => { renderActivityList(); + fireEvent.click(screen.getByLabelText(/Show Past Events:/i)); fireEvent.change(screen.getByLabelText(/Sort By:/i), { target: { value: 'latest' }, });