diff --git a/src/components/BMDashboard/WeeklyProjectSummary/Financials/ExpenseBarChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/Financials/ExpenseBarChart.jsx
index f1c4594a55..4db242b9d3 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/Financials/ExpenseBarChart.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/Financials/ExpenseBarChart.jsx
@@ -1,9 +1,217 @@
-import { BarChart, Bar, XAxis, YAxis, LabelList, ResponsiveContainer } from 'recharts';
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ LabelList,
+ ResponsiveContainer,
+ Tooltip,
+ Cell,
+} from 'recharts';
import { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
const categories = ['Plumbing', 'Electrical', 'Structural', 'Mechanical'];
const projects = ['Project A', 'Project B', 'Project C'];
+const rawData = [
+ {
+ projectId: 'Project A',
+ category: 'Plumbing',
+ plannedCost: 1000,
+ actualCost: 1200,
+ date: '2025-04-01',
+ },
+ {
+ projectId: 'Project A',
+ category: 'Electrical',
+ plannedCost: 1500,
+ actualCost: 1300,
+ date: '2025-04-01',
+ },
+ {
+ projectId: 'Project B',
+ category: 'Plumbing',
+ plannedCost: 1100,
+ actualCost: 1050,
+ date: '2025-04-02',
+ },
+ {
+ projectId: 'Project B',
+ category: 'Structural',
+ plannedCost: 2200,
+ actualCost: 2150,
+ date: '2025-04-02',
+ },
+ {
+ projectId: 'Project C',
+ category: 'Mechanical',
+ plannedCost: 1300,
+ actualCost: 1350,
+ date: '2025-04-03',
+ },
+ {
+ projectId: 'Project C',
+ category: 'Electrical',
+ plannedCost: 1400,
+ actualCost: 1600,
+ date: '2025-04-03',
+ },
+];
+
+const isDateMatch = (entryDate, startDate, endDate) => {
+ if (startDate && entryDate < startDate) return false;
+ if (endDate && entryDate > endDate) return false;
+ return true;
+};
+
+const isProjectMatch = (entryProject, projectId) => projectId === '' || entryProject === projectId;
+const isCategoryMatch = (entryCategory, categoryFilter) =>
+ categoryFilter === 'ALL' || entryCategory === categoryFilter;
+
+const getFilteredAndAggregatedData = (startDate, endDate, projectId, categoryFilter) => {
+ const filtered = rawData.filter(
+ entry =>
+ isDateMatch(entry.date, startDate, endDate) &&
+ isProjectMatch(entry.projectId, projectId) &&
+ isCategoryMatch(entry.category, categoryFilter),
+ );
+
+ const aggregated = {};
+ filtered.forEach(entry => {
+ const key = entry.projectId;
+ if (!aggregated[key]) {
+ aggregated[key] = { project: key, planned: 0, actual: 0 };
+ }
+ aggregated[key].planned += entry.plannedCost;
+ aggregated[key].actual += entry.actualCost;
+ });
+
+ return Object.values(aggregated).map(item => {
+ item.variance = item.actual - item.planned;
+ const percent = item.planned === 0 ? 0 : ((item.variance / item.planned) * 100).toFixed(1);
+ item.variancePercent = percent > 0 ? `+${percent}%` : `${percent}%`;
+ item.varianceLabel = item.variance > 0 ? `+$${item.variance}` : `-$${Math.abs(item.variance)}`;
+ return item;
+ });
+};
+
+const getTheme = darkMode => {
+ const mode = darkMode ? 'dark' : 'light';
+ return {
+ inputBg: { dark: '#2d3748', light: '#fff' }[mode],
+ inputText: { dark: '#f8fafc', light: '#0f172a' }[mode],
+ inputBorder: { dark: '1px solid #475569', light: '1px solid #cbd5e1' }[mode],
+ labelColor: { dark: '#e2e8f0', light: '#334155' }[mode],
+ titleColor: { dark: '#f8fafc', light: '#1e293b' }[mode],
+ legendColor: { dark: '#cbd5e1', light: '#334155' }[mode],
+ emptyTextColor: { dark: '#94a3b8', light: '#64748b' }[mode],
+ axisStroke: { dark: '#64748b', light: '#94a3b8' }[mode],
+ axisTick: { dark: '#cbd5e1', light: '#475569' }[mode],
+ cursorFill: { dark: 'rgba(255, 255, 255, 0.05)', light: 'rgba(0, 0, 0, 0.05)' }[mode],
+ tooltipBg: { dark: '#1e293b', light: '#ffffff' }[mode],
+ tooltipBorder: { dark: '1px solid #334155', light: '1px solid #e2e8f0' }[mode],
+ tooltipText: { dark: '#f8fafc', light: '#0f172a' }[mode],
+ tooltipHeaderBorder: { dark: '1px solid #475569', light: '1px solid #f1f5f9' }[mode],
+ tooltipHeaderColor: { dark: '#94a3b8', light: '#64748b' }[mode],
+ overBudget: { dark: '#ff5252', light: '#EA4335' }[mode],
+ underBudget: { dark: '#4ade80', light: '#34A853' }[mode],
+ };
+};
+
+const VarianceLabel = props => {
+ const { x, y, width, value, darkMode } = props;
+ const isOver = value?.toString().startsWith('+');
+ const theme = getTheme(darkMode);
+
+ return (
+
+ {value}
+
+ );
+};
+
+VarianceLabel.propTypes = {
+ x: PropTypes.number,
+ y: PropTypes.number,
+ width: PropTypes.number,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ darkMode: PropTypes.bool,
+};
+
+const CustomTooltip = ({ active, payload, label, darkMode }) => {
+ if (!active || !payload?.length) return null;
+
+ const chartData = payload[0].payload;
+ const isOverBudget = chartData.variance > 0;
+ const theme = getTheme(darkMode);
+
+ return (
+
+
+ {label}
+
+
+ Planned: ${chartData.planned.toLocaleString()}
+
+
+ Actual: ${chartData.actual.toLocaleString()}
+
+
+ Variance: {chartData.variance > 0 ? '+' : ''}${chartData.variance.toLocaleString()} (
+ {chartData.variancePercent})
+
+
+ );
+};
+
+CustomTooltip.propTypes = {
+ active: PropTypes.bool,
+ payload: PropTypes.arrayOf(
+ PropTypes.shape({
+ payload: PropTypes.shape({
+ planned: PropTypes.number,
+ actual: PropTypes.number,
+ variance: PropTypes.number,
+ variancePercent: PropTypes.string,
+ }),
+ }),
+ ),
+ label: PropTypes.string,
+ darkMode: PropTypes.bool,
+};
+
export default function ExpenseBarChart({ darkMode }) {
const [projectId, setProjectId] = useState('');
const [categoryFilter, setCategoryFilter] = useState('ALL');
@@ -12,109 +220,59 @@ export default function ExpenseBarChart({ darkMode }) {
const [data, setData] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
- // Dark Mode Styles for Inputs/Selects
+ const theme = getTheme(darkMode);
+
+ const today = new Date();
+ const localTodayString = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(
+ 2,
+ '0',
+ )}-${String(today.getDate()).padStart(2, '0')}`;
+
+ const labelGroupStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: '0.4rem',
+ color: theme.labelColor,
+ fontWeight: '600',
+ fontSize: '0.85rem',
+ width: '100%',
+ };
+
const inputStyle = {
- marginLeft: '0.3rem',
width: '100%',
- padding: '4px',
+ padding: '8px 10px',
borderRadius: '4px',
- backgroundColor: darkMode ? '#333' : '#fff',
- color: darkMode ? '#eee' : '#000',
- border: darkMode ? '1px solid #555' : '1px solid #ccc',
+ backgroundColor: theme.inputBg,
+ color: theme.inputText,
+ border: theme.inputBorder,
outline: 'none',
- };
-
- const labelStyle = {
- minWidth: '150px',
- color: darkMode ? '#bbb' : '#555',
+ boxSizing: 'border-box',
+ colorScheme: darkMode ? 'dark' : 'light',
};
useEffect(() => {
- async function fetchData() {
- try {
- const rawData = [
- {
- projectId: 'Project A',
- category: 'Plumbing',
- plannedCost: 1000,
- actualCost: 1200,
- date: '2025-04-01',
- },
- {
- projectId: 'Project A',
- category: 'Electrical',
- plannedCost: 1500,
- actualCost: 1300,
- date: '2025-04-01',
- },
- {
- projectId: 'Project B',
- category: 'Plumbing',
- plannedCost: 1100,
- actualCost: 1050,
- date: '2025-04-02',
- },
- {
- projectId: 'Project B',
- category: 'Structural',
- plannedCost: 2200,
- actualCost: 2150,
- date: '2025-04-02',
- },
- {
- projectId: 'Project C',
- category: 'Mechanical',
- plannedCost: 1300,
- actualCost: 1350,
- date: '2025-04-03',
- },
- {
- projectId: 'Project C',
- category: 'Electrical',
- plannedCost: 1400,
- actualCost: 1600,
- date: '2025-04-03',
- },
- ];
-
- const filtered = rawData.filter(entry => {
- const entryDate = new Date(entry.date);
- const start = startDate ? new Date(startDate) : null;
- const end = endDate ? new Date(endDate) : null;
- const dateMatch = (!start || entryDate >= start) && (!end || entryDate <= end);
- const projectMatch = projectId === '' || entry.projectId === projectId;
- const categoryMatch = categoryFilter === 'ALL' || entry.category === categoryFilter;
- return dateMatch && projectMatch && categoryMatch;
- });
-
- const aggregated = {};
- filtered.forEach(entry => {
- const key = entry.projectId;
- if (!aggregated[key]) {
- aggregated[key] = { project: key, planned: 0, actual: 0 };
- }
- aggregated[key].planned += entry.plannedCost;
- aggregated[key].actual += entry.actualCost;
- });
-
- setData(Object.values(aggregated));
- } catch (error) {
- setErrorMessage('Something went wrong while loading chart data.');
- }
+ try {
+ const processedData = getFilteredAndAggregatedData(
+ startDate,
+ endDate,
+ projectId,
+ categoryFilter,
+ );
+ setData(processedData);
+ } catch (error) {
+ setErrorMessage('Something went wrong while loading chart data.');
}
- fetchData();
}, [projectId, categoryFilter, startDate, endDate]);
return (
-
-
-
+
+
+
Planned vs Actual Cost
{errorMessage && (
-
+
{errorMessage}
)}
@@ -122,16 +280,14 @@ export default function ExpenseBarChart({ darkMode }) {
-
- {/* Legend */}
-
+
{' '}
Planned
-
+
{' '}
- Actual
+ Actual (Over Budget)
+
+
+ {' '}
+ Actual (Under Budget)
-
-
-
-
-
-
-
+ {data.length === 0 && !errorMessage ? (
+
+ No data available for the selected filters.
+
+ ) : (
+
+
+
-
-
- `$${value.toLocaleString()}`}
/>
-
-
-
+
}
+ cursor={{ fill: theme.cursorFill }}
+ wrapperStyle={{ backgroundColor: 'transparent', outline: 'none' }}
+ contentStyle={{ backgroundColor: 'transparent', border: 'none' }}
+ />
+
+ `$${val.toLocaleString()}`}
+ />
+
+
+ }
+ />
+ {data.map(entry => (
+ 0 ? theme.overBudget : theme.underBudget}
+ />
+ ))}
+ |
+
+
+ )}
);
}
+
+ExpenseBarChart.propTypes = {
+ darkMode: PropTypes.bool,
+};
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
index bdcb33cfa3..901dc54ff9 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
@@ -351,10 +351,9 @@ function WeeklyProjectSummary() {
key: 'Financials',
className: 'large',
content: (
-
-
📊 Card
+
-
+
@@ -422,7 +421,7 @@ function WeeklyProjectSummary() {
}),
},
],
- [quantityOfMaterialsUsedData],
+ [quantityOfMaterialsUsedData, darkMode],
);
const handleSaveAsPDF = async () => {