Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 (
<div className={`${cardStyles.varianceCard} ${cardClass}`}>
<div className={cardStyles.varianceCardCategory}>{item.category}</div>
<div className={cardStyles.varianceCardRow}>
<span>Planned:</span>
<span>{item.plannedCost.toLocaleString()}</span>
</div>
<div className={cardStyles.varianceCardRow}>
<span>Actual:</span>
<span>{item.actualCost.toLocaleString()}</span>
</div>
<div className={cardStyles.varianceCardRow}>
<span>Variance:</span>
<span>
{isOverrun ? '+' : ''}
{item.variance.toLocaleString()}
</span>
</div>
{item.variancePct !== null && (
<div className={cardStyles.varianceCardPct}>
{isOverrun ? '+' : ''}
{item.variancePct.toFixed(1)}%
</div>
)}
<div className={cardStyles.varianceCardStatus}>{item.budgetStatus}</div>
</div>
);
}

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 (
<div
style={{
display: 'flex',
height: 200,
justifyContent: 'center',
alignItems: 'center',
color: 'var(--text-color)',
}}
>
<Spinner color="primary" size="sm" />
<span style={{ marginLeft: '10px' }}>Updating chart...</span>
</div>
);
}
if (hasData) {
return (
<div style={{ width: '100%', height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={chartDataWithVariance}
margin={{ top: 20, right: 5, left: 5, bottom: 0 }}
barGap={20}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="category"
axisLine={false}
tickLine={false}
tick={{ fill: 'var(--text-color)' }}
/>
<YAxis tick={{ fill: 'var(--text-color)', fontSize: '12px' }} />
<Tooltip
cursor={{ fill: 'transparent' }}
allowEscapeViewBox={{ x: true, y: true }}
contentStyle={{
backgroundColor: darkMode ? '#1f242b' : 'var(--card-bg)',
borderColor: darkMode ? '#45505e' : 'var(--button-hover)',
borderRadius: '6px',
color: 'var(--text-color)',
}}
labelStyle={{ color: 'var(--text-color)', fontSize: '12px' }}
itemStyle={{ color: 'var(--text-color)', fontSize: '12px' }}
wrapperStyle={{ pointerEvents: 'none', zIndex: 12 }}
/>
<Legend
verticalAlign="top"
height={36}
iconSize={8}
wrapperStyle={{ color: 'var(--text-color)' }}
/>
<Bar
dataKey="actualCost"
name="Actual"
fill={darkMode ? '#c0392b' : '#e74a3b'}
barSize={40}
>
<LabelList dataKey="actualCost" position="top" fill="var(--text-color)" />
</Bar>
<Bar
dataKey="plannedCost"
name="Planned"
fill={!darkMode ? '#17a272' : '#1cc88a'}
barSize={40}
>
<LabelList dataKey="plannedCost" position="top" fill="var(--text-color)" />
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
);
}
return (
<div
style={{
display: 'flex',
height: 200,
justifyContent: 'center',
alignItems: 'center',
color: 'var(--text-color)',
fontStyle: 'italic',
}}
>
No data available for the selected filters.
</div>
);
}

function ActualVsPlannedCost() {
const dispatch = useDispatch();
const projects = useSelector(state => state.bmProjects) || [];
Expand Down Expand Up @@ -105,90 +261,38 @@ function ActualVsPlannedCost() {

const filterSummary = `${selectedProjectName || 'Loading...'} - ${selectedCategory}`;

let chartContent;
if (loading || isFiltering) {
chartContent = (
<div
style={{
display: 'flex',
height: 200,
justifyContent: 'center',
alignItems: 'center',
color: 'var(--text-color)',
}}
>
<Spinner color="primary" size="sm" />
<span style={{ marginLeft: '10px' }}>Updating chart...</span>
</div>
);
} else if (
!chartData.length ||
(chartData.length === 1 && chartData[0].actualCost === 0 && chartData[0].plannedCost === 0)
) {
chartContent = (
<div
style={{
display: 'flex',
height: 200,
justifyContent: 'center',
alignItems: 'center',
color: 'var(--text-color)',
fontStyle: 'italic',
}}
>
No data available for the selected filters.
</div>
);
} else {
chartContent = (
<div style={{ width: '100%', height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 20, right: 5, left: 5, bottom: 0 }} barGap={20}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="category"
axisLine={false}
tickLine={false}
tick={{ fill: 'var(--text-color)' }}
/>
<YAxis tick={{ fill: 'var(--text-color)', fontSize: '12px' }} />
<Tooltip
contentStyle={{
backgroundColor: 'var(--card-bg)',
borderColor: 'var(--button-hover)',
}}
labelStyle={{ color: 'var(--text-color)', fontSize: '12px' }}
/>
<Legend
verticalAlign="top"
height={36}
iconSize={8}
wrapperStyle={{ color: 'var(--text-color)' }}
/>
<Bar
dataKey="actualCost"
name="Actual"
fill={darkMode ? '#c0392b' : '#e74a3b'}
barSize={40}
>
<LabelList dataKey="actualCost" position="top" fill="var(--text-color)" />
</Bar>
<Bar
dataKey="plannedCost"
name="Planned"
fill={!darkMode ? '#17a272' : '#1cc88a'}
barSize={40}
>
<LabelList dataKey="plannedCost" position="top" fill="var(--text-color)" />
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
const chartDataWithVariance = chartData.map(item => {
const variance = item.actualCost - item.plannedCost;
return {
...item,
variance,
variancePct: item.plannedCost > 0 ? (variance / item.plannedCost) * 100 : null,
budgetStatus: getBudgetStatus(variance),
};
});

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;

const chartContent = buildChartContent({
loading,
isFiltering,
hasData,
chartDataWithVariance,
darkMode,
});

return (
<div style={{ padding: 10 }}>
<div style={{ padding: 10 }} className={darkMode ? styles.darkMode : ''}>
<div style={{ textAlign: 'center', marginBottom: '15px' }}>
<h2 style={{ fontSize: 'large', margin: '0 0 5px 0' }} className={styles.title}>
Actual vs Planned Costs
Expand Down Expand Up @@ -234,6 +338,26 @@ function ActualVsPlannedCost() {
</div>

{chartContent}

{!loading && !isFiltering && hasData && (
<div className={styles.varianceSummaryContainer}>
<div className={styles.varianceSummaryHeader}>
<h3 className={styles.varianceSummaryTitle}>Variance and Budget Indicators</h3>
<div className={isTotalOverrun ? styles.totalOverrunBadge : styles.totalOnTrackBadge}>
Total Variance: {isTotalOverrun ? '+' : ''}
{totalVariance.toLocaleString()}
{totalVariancePct !== null &&
` (${isTotalOverrun ? '+' : ''}${totalVariancePct.toFixed(1)}%)`}
</div>
</div>

<div className={styles.varianceCardsRow}>
{chartDataWithVariance.map(item => (
<VarianceCard key={item.category} item={item} cardStyles={styles} />
))}
</div>
</div>
)}
</div>
);
}
Expand Down
Loading
Loading