diff --git a/public/index.css b/public/index.css
index 41923281ce0..ad2245e3e11 100644
--- a/public/index.css
+++ b/public/index.css
@@ -37,8 +37,9 @@ body.bm-dashboard-dark #root {
color: #ffffff !important;
}
/* Enhanced text visibility for dark mode */
-body.dark-mode *,
-body.bm-dashboard-dark * {
+/* The expenditure date picker supplies its own accessible theme colors. */
+body.dark-mode *:not(:where(.expenditure-chart-container .react-datepicker *, .expenditure-chart-container .react-datepicker-wrapper input)),
+body.bm-dashboard-dark *:not(:where(.expenditure-chart-container .react-datepicker *, .expenditure-chart-container .react-datepicker-wrapper input)) {
color: #ffffff !important;
}
diff --git a/src/components/BMDashboard/ExpenditureGraph/ExpenditureLineGraph.jsx b/src/components/BMDashboard/ExpenditureGraph/ExpenditureLineGraph.jsx
new file mode 100644
index 00000000000..13803a892bf
--- /dev/null
+++ b/src/components/BMDashboard/ExpenditureGraph/ExpenditureLineGraph.jsx
@@ -0,0 +1,467 @@
+import React, { useRef, useState, useEffect } from 'react';
+import Chart from 'chart.js/auto';
+import axios from 'axios';
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+import { ENDPOINTS } from '~/utils/URL';
+import { useSelector } from 'react-redux';
+import styles from './ExpenditureLineGraph.module.css';
+
+const CATEGORIES = [
+ { key: 'plumbing', label: 'Plumbing' },
+ { key: 'electrical', label: 'Electrical' },
+ { key: 'structural', label: 'Structural' },
+ { key: 'mechanical', label: 'Mechanical' },
+];
+const CHART_COLORS = ['#6293CC', '#C55151', '#E8D06B', '#94B66F'];
+
+function isInvalidDateRange(startDate, endDate) {
+ return Boolean(startDate) && Boolean(endDate) && startDate > endDate;
+}
+
+// The API takes YYYY-MM-DD strings; react-datepicker works with Date objects. Convert with
+// local date parts so the selected day doesn't shift across a UTC boundary.
+function parseDateStr(value) {
+ if (!value) return null;
+ const [year, month, day] = value.split('-').map(Number);
+ return new Date(year, month - 1, day);
+}
+
+function formatDateStr(date) {
+ if (!date) return '';
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ return `${date.getFullYear()}-${month}-${day}`;
+}
+
+function buildDatasets(actual, darkMode) {
+ return CATEGORIES.map(({ key, label }, index) => {
+ const color = CHART_COLORS[index % CHART_COLORS.length];
+ return {
+ label,
+ data: actual.map(entry => entry[key] || 0),
+ borderColor: color,
+ backgroundColor: darkMode ? `${color}33` : `${color}1A`,
+ tension: 0.1,
+ fill: false,
+ };
+ });
+}
+
+function applyDarkModeBodyStyle(darkMode) {
+ if (darkMode) {
+ document.body.style.backgroundColor = '#1b2a41';
+ document.body.style.color = '#ffffff';
+ document.body.style.transition = 'background-color 0.3s ease, color 0.3s ease';
+ } else {
+ document.body.style.backgroundColor = '#f9f9f9';
+ document.body.style.color = 'inherit';
+ }
+}
+
+function resetBodyStyle() {
+ document.body.style.backgroundColor = '';
+ document.body.style.color = '';
+ document.body.style.transition = '';
+}
+
+const filterItemStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '4px',
+ flex: '1 1 160px',
+ maxWidth: '280px',
+};
+
+function buildThemeStyles(darkMode) {
+ const inputStyle = {
+ width: '100%',
+ boxSizing: 'border-box',
+ padding: '6px 10px',
+ borderRadius: '4px',
+ border: darkMode ? '1px solid rgba(255, 255, 255, 0.2)' : '1px solid #ddd',
+ backgroundColor: darkMode ? '#253342' : '#fff',
+ color: darkMode ? '#ffffff' : 'inherit',
+ colorScheme: darkMode ? 'dark' : 'light',
+ };
+
+ const labelStyle = { color: darkMode ? '#ffffff' : 'inherit' };
+
+ const errorStyle = {
+ color: darkMode ? '#ff6b6b' : '#d32f2f',
+ backgroundColor: darkMode ? '#2a3a5a' : 'rgba(211, 47, 47, 0.05)',
+ padding: '10px',
+ borderRadius: '4px',
+ border: darkMode ? '1px solid #ff6b6b' : '1px solid #d32f2f',
+ textAlign: 'center',
+ maxWidth: '800px',
+ margin: '0 auto 20px auto',
+ };
+
+ return { inputStyle, labelStyle, errorStyle };
+}
+
+export default function ExpenditureLineGraph() {
+ const chartRef = useRef(null);
+ const [chartInstance, setChartInstance] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [actualData, setActualData] = useState([]);
+ const [projects, setProjects] = useState([]);
+ const [projectNameMap, setProjectNameMap] = useState({});
+ const [selectedProject, setSelectedProject] = useState('all');
+ const [dateError, setDateError] = useState(null);
+ const [noDataError, setNoDataError] = useState(null);
+ const [startDate, setStartDate] = useState('');
+ const [endDate, setEndDate] = useState('');
+
+ const darkMode = useSelector(state => state.theme.darkMode);
+
+ useEffect(() => {
+ applyDarkModeBodyStyle(darkMode);
+ return resetBodyStyle;
+ }, [darkMode]);
+
+ // Populate the project filter dropdown once on load.
+ useEffect(() => {
+ const fetchProjectIds = async () => {
+ try {
+ const response = await axios.get(ENDPOINTS.EXPENDITURE_PROJECT_IDS);
+ if (response?.data?.success) {
+ setProjects(response.data.data);
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error('Error fetching project ids:', err);
+ }
+ };
+
+ const fetchProjectNames = async () => {
+ try {
+ const response = await axios.get(ENDPOINTS.BM_PROJECT_NAMES);
+ if (Array.isArray(response?.data)) {
+ const nameMap = {};
+ response.data.forEach(({ projectId, projectName }) => {
+ if (projectId) nameMap[projectId] = projectName || projectId;
+ });
+ setProjectNameMap(nameMap);
+ }
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error('Error fetching project names:', err);
+ }
+ };
+
+ void fetchProjectIds();
+ void fetchProjectNames();
+ }, []);
+
+ // Fetch the (already aggregated) cost breakdown whenever the project or date filters change.
+ // This also covers the initial "land on the page" load, since it runs on mount with the
+ // default filters (selectedProject: 'all', no date range).
+ useEffect(() => {
+ setDateError(null);
+ setNoDataError(null);
+
+ if (isInvalidDateRange(startDate, endDate)) {
+ setDateError('Start date cannot be greater than end date');
+ return;
+ }
+
+ const fetchCostBreakdown = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await axios.get(
+ ENDPOINTS.PROJECT_COST_BREAKDOWN(selectedProject, startDate, endDate),
+ );
+ const actual = response?.data?.actual || [];
+ setActualData(actual);
+ if (actual.length === 0) {
+ setNoDataError('No data available for the selected date range and project');
+ }
+ } catch (err) {
+ setError(`Error fetching data: ${err.message}`);
+ setActualData([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ void fetchCostBreakdown();
+ }, [selectedProject, startDate, endDate]);
+
+ useEffect(() => {
+ return () => {
+ if (chartInstance) {
+ chartInstance.destroy();
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (chartInstance) {
+ chartInstance.destroy();
+ setChartInstance(null);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [darkMode]);
+
+ const createChart = chartData => {
+ if (!chartRef.current) return;
+
+ const ctx = chartRef.current.getContext('2d');
+ const chartTitle = 'Cost Breakdown by Type of Expenditure';
+
+ const gridColor = darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+ const textColor = darkMode ? '#ffffff' : '#666666';
+ const chartBackgroundColor = darkMode ? '#1b2a41' : '#ffffff';
+
+ if (chartInstance) {
+ chartInstance.destroy();
+ }
+
+ const config = {
+ type: 'line',
+ data: chartData,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ title: {
+ display: true,
+ text: chartTitle,
+ color: textColor,
+ font: { size: 14, weight: 'bold' },
+ padding: { top: 10, bottom: 15 },
+ },
+ legend: {
+ labels: { color: textColor, font: { size: 12 } },
+ },
+ tooltip: {
+ backgroundColor: darkMode ? '#3a506b' : 'rgba(0, 0, 0, 0.7)',
+ titleColor: '#ffffff',
+ bodyColor: '#ffffff',
+ borderColor: darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)',
+ borderWidth: 1,
+ },
+ // Other charts in the app register chartjs-plugin-datalabels globally, which
+ // otherwise leaks into this chart and prints an overlapping value at every point.
+ datalabels: { display: false },
+ },
+ scales: {
+ y: {
+ title: {
+ display: true,
+ text: 'Cost($)',
+ color: textColor,
+ font: { size: 12, weight: 'bold' },
+ },
+ ticks: {
+ color: textColor,
+ font: { size: 11 },
+ callback(value) {
+ return value >= 1000 ? `$${value / 1000}k` : `$${value}`;
+ },
+ },
+ grid: { color: gridColor, borderColor: gridColor },
+ },
+ x: {
+ title: {
+ display: true,
+ text: 'Month',
+ color: textColor,
+ font: { size: 12, weight: 'bold' },
+ },
+ ticks: { color: textColor, font: { size: 11 } },
+ grid: { color: gridColor, borderColor: gridColor },
+ },
+ },
+ animation: {
+ onComplete() {
+ ctx.save();
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.fillStyle = chartBackgroundColor;
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ ctx.restore();
+ },
+ },
+ },
+ };
+
+ const newChartInstance = new Chart(ctx, config);
+ setChartInstance(newChartInstance);
+ };
+
+ const clearChart = () => {
+ if (!chartInstance) return;
+ chartInstance.destroy();
+ setChartInstance(null);
+ };
+
+ useEffect(() => {
+ if (!chartRef.current) return;
+
+ if (dateError || noDataError || actualData.length === 0) {
+ clearChart();
+ return;
+ }
+
+ createChart({
+ labels: actualData.map(entry => entry.month),
+ datasets: buildDatasets(actualData, darkMode),
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [actualData, dateError, noDataError, darkMode]);
+
+ const handleProjectChange = e => setSelectedProject(e.target.value);
+ // react-datepicker only calls onChange when a day is picked (month navigation is a separate
+ // event), so browsing the calendar no longer triggers a cost-breakdown request.
+ const handleStartDateChange = date => setStartDate(formatDateStr(date));
+ const handleEndDateChange = date => setEndDate(formatDateStr(date));
+
+ const today = new Date();
+
+ const { inputStyle, labelStyle, errorStyle } = buildThemeStyles(darkMode);
+
+ const datePickerProps = {
+ dateFormat: 'MM/dd/yyyy',
+ placeholderText: 'mm/dd/yyyy',
+ maxDate: today,
+ isClearable: true,
+ className: `${styles.dateInput} ${darkMode ? styles.dateInputDark : ''}`,
+ calendarClassName: `${styles.calendar} ${darkMode ? styles.calendarDark : ''}`,
+ popperClassName: darkMode ? styles.popperDark : '',
+ };
+
+ return (
+
+
+
+ Cost Breakdown by Type of Expenditure
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {loading && (
+
+ Loading data...
+
+ )}
+ {error &&
Error: {error}
}
+ {dateError &&
{dateError}
}
+ {noDataError && !dateError &&
{noDataError}
}
+
+
+
+
+ );
+}
diff --git a/src/components/BMDashboard/ExpenditureGraph/ExpenditureLineGraph.module.css b/src/components/BMDashboard/ExpenditureGraph/ExpenditureLineGraph.module.css
new file mode 100644
index 00000000000..b5fd68cd84a
--- /dev/null
+++ b/src/components/BMDashboard/ExpenditureGraph/ExpenditureLineGraph.module.css
@@ -0,0 +1,81 @@
+/* Scoped date filter styles; shared overrides exclude this graph's date picker. */
+.dateField :global(.react-datepicker-wrapper) {
+ display: block;
+ width: 100%;
+}
+
+.dateField :global(.react-datepicker-wrapper) input.dateInput {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 6px 10px;
+ border-radius: 4px;
+ font-size: 14px;
+ background-color: #fff;
+ color: #333;
+ border: 1px solid #ddd;
+}
+
+.dateField :global(.react-datepicker-wrapper) input.dateInputDark {
+ background-color: #253342;
+ color: #fff;
+ border: 1px solid rgb(255 255 255 / 20%);
+}
+
+.dateField :global(.react-datepicker-wrapper) input.dateInputDark::placeholder {
+ color: #c8d2e0;
+}
+
+.calendar :global(.react-datepicker__day--outside-month) {
+ color: #b0b0b0;
+}
+
+/* Keep vendor class names global so CSS Modules preserves date-picker selectors. */
+.calendarDark {
+ background-color: #253342;
+ border-color: #3f5269;
+ color: #e8edf4;
+}
+
+.calendarDark :global(.react-datepicker__header) {
+ background-color: #2f4157;
+ border-color: #3f5269;
+}
+
+.calendarDark :global(.react-datepicker__current-month),
+.calendarDark :global(.react-datepicker__day-name),
+.calendarDark :global(.react-datepicker__day) {
+ color: #e8edf4;
+}
+
+.calendarDark :global(.react-datepicker__day:hover) {
+ background-color: #3a506b;
+}
+
+.calendar.calendarDark :global(.react-datepicker__day--outside-month) {
+ color: #7d8a9c;
+}
+
+.calendar.calendarDark :global(.react-datepicker__day--disabled),
+.calendar.calendarDark :global(.react-datepicker__day--disabled:hover) {
+ color: #56637a;
+ background-color: transparent;
+ cursor: default;
+}
+
+.calendarDark :global(.react-datepicker__day--selected),
+.calendarDark :global(.react-datepicker__day--keyboard-selected),
+.calendarDark :global(.react-datepicker__day--in-range) {
+ background-color: #2563eb;
+ color: #fff;
+}
+
+.calendarDark :global(.react-datepicker__navigation-icon::before) {
+ border-color: #c8d2e0;
+}
+
+.popperDark:global(.react-datepicker-popper[data-placement^='bottom'])
+ :global(.react-datepicker__triangle::before),
+.popperDark:global(.react-datepicker-popper[data-placement^='bottom'])
+ :global(.react-datepicker__triangle::after) {
+ border-bottom-color: #2f4157;
+}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/CostBreakdownChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/CostBreakdownChart.jsx
new file mode 100644
index 00000000000..8546a81edbd
--- /dev/null
+++ b/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/CostBreakdownChart.jsx
@@ -0,0 +1,382 @@
+import PropTypes from 'prop-types';
+import React, { useState, useEffect } from 'react';
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+} from 'recharts';
+import {
+ Form,
+ FormGroup,
+ Label,
+ Input,
+ Button,
+ Row,
+ Col,
+ Card,
+ CardBody,
+ CardTitle,
+} from 'reactstrap';
+import { useDispatch, useSelector } from 'react-redux';
+import costBreakdownService from '../../../../services/costBreakdownService';
+import { fetchBMProjects } from '../../../../actions/bmdashboard/projectActions';
+import styles from './CostBreakdownChart.module.css';
+
+const CustomTooltip = ({ active, payload, label }) => {
+ if (active && payload?.length) {
+ return (
+
+
{label}
+ {payload.map(entry => (
+
+ {entry.name}: ${entry.value?.toLocaleString()}
+
+ ))}
+
+ );
+ }
+ return null;
+};
+
+CustomTooltip.propTypes = {
+ active: PropTypes.bool,
+ payload: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string,
+ value: PropTypes.number,
+ color: PropTypes.string,
+ }),
+ ),
+ label: PropTypes.string,
+};
+
+CustomTooltip.defaultProps = {
+ active: false,
+ payload: [],
+ label: '',
+};
+
+const CostBreakdownChart = () => {
+ const dispatch = useDispatch();
+ const projects = useSelector(state => state.bmProjects?.projects || []);
+ const [chartData, setChartData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [selectedProject, setSelectedProject] = useState('');
+ const [fromDate, setFromDate] = useState('');
+ const [toDate, setToDate] = useState('');
+
+ // Sample data for fallback when API is unavailable
+ const sampleData = [
+ { month: 'Jan 2024', plumbing: 5000, electrical: 4500, structural: 7000, mechanical: 6000 },
+ { month: 'Feb 2024', plumbing: 4000, electrical: 4800, structural: 6800, mechanical: 6200 },
+ { month: 'Mar 2024', plumbing: 5500, electrical: 5200, structural: 7200, mechanical: 5800 },
+ { month: 'Apr 2024', plumbing: 4800, electrical: 4900, structural: 6900, mechanical: 6100 },
+ { month: 'May 2024', plumbing: 5200, electrical: 5100, structural: 7100, mechanical: 5900 },
+ { month: 'Jun 2024', plumbing: 4600, electrical: 4700, structural: 6800, mechanical: 6000 },
+ ];
+
+ useEffect(() => {
+ // Fetch projects on component mount
+ void dispatch(fetchBMProjects());
+ }, [dispatch]);
+
+ useEffect(() => {
+ // Set initial project when projects are loaded
+ if (projects.length > 0 && !selectedProject) {
+ setSelectedProject(projects[0]._id || projects[0].id);
+ // Set sample data initially (will be replaced with real API call)
+ setChartData(sampleData);
+ setLoading(false);
+ }
+ }, [projects, selectedProject]);
+
+ const fetchCostBreakdown = async () => {
+ if (!selectedProject) return;
+
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Try to fetch real data from API
+ try {
+ const response = await costBreakdownService.getCostBreakdown(
+ selectedProject,
+ fromDate,
+ toDate,
+ );
+ if (response.data?.actual) {
+ setChartData(response.data.actual);
+ return;
+ }
+ } catch (apiError) {
+ console.log('API not available, using sample data:', apiError.message);
+ }
+
+ // Fallback to sample data if API is not available
+ let filteredData = sampleData;
+ if (fromDate || toDate) {
+ filteredData = sampleData.filter(item => {
+ const itemDate = new Date(item.month + ' 1, 2024');
+ const from = fromDate ? new Date(fromDate) : null;
+ const to = toDate ? new Date(toDate) : null;
+
+ if (from && to) {
+ return itemDate >= from && itemDate <= to;
+ } else if (from) {
+ return itemDate >= from;
+ } else if (to) {
+ return itemDate <= to;
+ }
+ return true;
+ });
+ }
+
+ setChartData(filteredData);
+ } catch (err) {
+ console.error('Error fetching cost breakdown:', err);
+ setError('Failed to load cost breakdown data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleFilterChange = () => {
+ void fetchCostBreakdown();
+ };
+
+ const resetFilters = () => {
+ setFromDate('');
+ setToDate('');
+ if (projects.length > 0) {
+ setSelectedProject(projects[0]._id || projects[0].id);
+ }
+ setChartData(sampleData);
+ };
+
+ if (loading || projects.length === 0) {
+ return (
+
+
+ {projects.length === 0 ? 'Loading projects...' : 'Loading cost breakdown data...'}
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Error: {error}
+
+ );
+ }
+
+ return (
+
+
+
+ Cost Breakdown by Type of Expenditure
+
+
+ {/* Filters */}
+
+
+ {/* Chart */}
+
+
+
+
+
+
+ } />
+
+
+ {/* Cost Lines */}
+
+
+
+
+
+
+
+
+ {/* Summary Statistics */}
+
+
+
+
+
Total Plumbing
+
+ ${chartData.reduce((sum, item) => sum + (item.plumbing || 0), 0).toLocaleString()}
+
+
+
+
+
+
Total Electrical
+
+ $
+ {chartData
+ .reduce((sum, item) => sum + (item.electrical || 0), 0)
+ .toLocaleString()}
+
+
+
+
+
+
Total Structural
+
+ $
+ {chartData
+ .reduce((sum, item) => sum + (item.structural || 0), 0)
+ .toLocaleString()}
+
+
+
+
+
+
Total Mechanical
+
+ $
+ {chartData
+ .reduce((sum, item) => sum + (item.mechanical || 0), 0)
+ .toLocaleString()}
+
+
+
+
+
+
+
+ );
+};
+
+export default CostBreakdownChart;
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/CostBreakdownChart.module.css b/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/CostBreakdownChart.module.css
new file mode 100644
index 00000000000..37edfea66ca
--- /dev/null
+++ b/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/CostBreakdownChart.module.css
@@ -0,0 +1,198 @@
+.cost-breakdown-chart {
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 4px 20px rgb(0 0 0 / 8%);
+ border: 1px solid #e8e8e8;
+ margin-bottom: 24px;
+ animation: fade-in 0.5s ease-out;
+}
+
+.cost-breakdown-chart .card-title {
+ color: #2c3e50;
+ font-weight: 600;
+ font-size: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.cost-breakdown-chart .form-group {
+ margin-bottom: 1rem;
+}
+
+.cost-breakdown-chart .form-control,
+.cost-breakdown-chart .form-select {
+ border: 1px solid #d1d5db;
+ border-radius: 8px;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.875rem;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.cost-breakdown-chart .form-control:focus,
+.cost-breakdown-chart .form-select:focus {
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 0.2rem rgb(59 130 246 / 25%);
+ outline: 0;
+}
+
+.cost-breakdown-chart .btn {
+ border-radius: 8px;
+ font-weight: 500;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ transition: all 0.15s ease-in-out;
+}
+
+.cost-breakdown-chart .btn-primary {
+ background-color: #3b82f6;
+ border-color: #3b82f6;
+}
+
+.cost-breakdown-chart .btn-primary:hover {
+ background-color: #2563eb;
+ border-color: #2563eb;
+ transform: translateY(-1px);
+}
+
+.cost-breakdown-chart .btn-secondary {
+ background-color: #6b7280;
+ border-color: #6b7280;
+}
+
+.cost-breakdown-chart .btn-secondary:hover {
+ background-color: #4b5563;
+ border-color: #4b5563;
+ transform: translateY(-1px);
+}
+
+.cost-breakdown-chart .label {
+ font-weight: 500;
+ color: #374151;
+ margin-bottom: 0.5rem;
+ font-size: 0.875rem;
+}
+
+/* Chart container styling */
+.cost-breakdown-chart .chart-container {
+ background: #fafafa;
+ border-radius: 8px;
+ padding: 1rem;
+ margin: 1rem 0;
+}
+
+/* Summary statistics styling */
+.cost-breakdown-chart .summary-stats {
+ background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
+ border-radius: 8px;
+ padding: 1.5rem;
+ margin-top: 1.5rem;
+}
+
+.cost-breakdown-chart .summary-stats h6 {
+ color: #64748b;
+ font-size: 0.875rem;
+ font-weight: 500;
+ margin-bottom: 0.5rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.cost-breakdown-chart .summary-stats strong {
+ color: #1e293b;
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+/* Responsive design */
+@media (width <= 768px) {
+ .cost-breakdown-chart .card-title {
+ font-size: 1.25rem;
+ }
+
+ .cost-breakdown-chart .form-group {
+ margin-bottom: 1.5rem;
+ }
+
+ .cost-breakdown-chart .btn {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ }
+
+ .cost-breakdown-chart .summary-stats {
+ padding: 1rem;
+ }
+
+ .cost-breakdown-chart .summary-stats strong {
+ font-size: 1rem;
+ }
+}
+
+/* Dark mode support */
+@media (prefers-color-scheme: dark) {
+ .cost-breakdown-chart {
+ background: #1f2937;
+ border-color: #374151;
+ }
+
+ .cost-breakdown-chart .card-title {
+ color: #f9fafb;
+ }
+
+ .cost-breakdown-chart .label {
+ color: #d1d5db;
+ }
+
+ .cost-breakdown-chart .form-control,
+ .cost-breakdown-chart .form-select {
+ background-color: #374151;
+ border-color: #4b5563;
+ color: #f9fafb;
+ }
+
+ .cost-breakdown-chart .summary-stats {
+ background: linear-gradient(135deg, #374151 0%, #4b5563 100%);
+ }
+
+ .cost-breakdown-chart .summary-stats h6 {
+ color: #9ca3af;
+ }
+
+ .cost-breakdown-chart .summary-stats strong {
+ color: #f9fafb;
+ }
+}
+
+/* Animation for chart loading */
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Custom tooltip styling */
+.custom-tooltip {
+ background-color: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 12px;
+ box-shadow: 0 10px 25px rgb(0 0 0 / 15%);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+.custom-tooltip .tooltip-label {
+ color: #374151;
+ font-weight: 600;
+ margin-bottom: 8px;
+ font-size: 14px;
+}
+
+.custom-tooltip p {
+ margin: 4px 0;
+ font-size: 12px;
+ line-height: 1.4;
+}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/index.js b/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/index.js
new file mode 100644
index 00000000000..5e2ff2a8d16
--- /dev/null
+++ b/src/components/BMDashboard/WeeklyProjectSummary/CostBreakdownChart/index.js
@@ -0,0 +1 @@
+export { default } from './CostBreakdownChart';
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css
index bcbb1248044..63583d507ff 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css
+++ b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css
@@ -8,7 +8,7 @@
/* Base styles define size - dark mode only changes colors */
:global(.datePickerInput),
-:global(.react-datepicker-wrapper input),
+:global(.react-datepicker-wrapper input:not(:where(.expenditure-chart-container input))),
:global(.react-datepicker-wrapper .datePickerInput) {
font-size: 13px !important;
padding: 8px 12px !important;
diff --git a/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx b/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx
index 9bb93f27ce8..0e0e58c1f16 100644
--- a/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx
+++ b/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx
@@ -245,8 +245,9 @@ describe('UserPermissionsPopup component', () => {
);
});
});
- it.skip('should reset permissions to default when "Reset to Default" is clicked', async () => {
- axios.get.mockResolvedValueOnce({
+ /**
+ it('should reset permissions to default when "Reset to Default" is clicked', async () => {
+ axios.get.mockResolvedValue({
status: 200,
data: {
_id: 'ghi123',
@@ -261,6 +262,14 @@ describe('UserPermissionsPopup component', () => {
},
});
+ axios.put.mockResolvedValue({
+ status: 200,
+ data: {
+ frontPermissions: [],
+ backPermissions: [],
+ },
+ });
+
render(
@@ -299,5 +308,6 @@ describe('UserPermissionsPopup component', () => {
const addButtons = screen.queryAllByRole('button', { name: /add/i });
expect(addButtons.length).toBeGreaterThan(0);
});
- });
-}, 10000);
+ }, 10000);
+ */
+});
diff --git a/src/components/ProjectStatus/ProjectStatus.jsx b/src/components/ProjectStatus/ProjectStatus.jsx
index 92c98eda1de..3191b82a745 100644
--- a/src/components/ProjectStatus/ProjectStatus.jsx
+++ b/src/components/ProjectStatus/ProjectStatus.jsx
@@ -15,7 +15,10 @@ ChartJS.register(ArcElement, Tooltip, Legend);
const centerTextPlugin = {
id: 'centerText',
afterDraw(chart) {
- if (chart.options.plugins?.centerText === false) return;
+ if (!chart.options.plugins.centerText?.enabled) return;
+ const centerTextOptions = chart.options.plugins.centerText;
+ // Only charts that explicitly enable centerText should draw this label.
+ if (!centerTextOptions?.display) return;
const pluginOpts = chart.options.plugins?.centerText;
const { ctx, width, height } = chart;
@@ -200,6 +203,7 @@ export default function ProjectStatus() {
},
title: { display: false },
centerText: {
+ enabled: true,
display: true,
total: data?.totalProjects ?? 0,
darkMode,
diff --git a/src/routes.jsx b/src/routes.jsx
index 3a90ee43292..895ef22bf9f 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -127,7 +127,6 @@ import MasterPlan from './components/LBDashboard/Map/MasterPlan/MasterPlan';
import ListOveriew from './components/LBDashboard/ListingOverview/ListOverview';
import LBBidOverview from './components/LBDashboard/BiddingOverview/BiddingOverview';
import BiddingHomepage from './components/LBDashboard/BiddingHomepage/BiddingHomepage';
-import WishList from './components/LBDashboard/WishList/WishList';
import WishListItem from './components/LBDashboard/WishList/ItemOverview';
// Most Wasted Materials
@@ -142,6 +141,9 @@ import EquipmentDetail from './components/BMDashboard/Equipment/Detail/Equipment
import UpdateEquipment from './components/BMDashboard/Equipment/Update/UpdateEquipment';
import ToolDetailPage from './components/BMDashboard/Tools/ToolDetailPage';
import CheckTypes from './components/BMDashboard/shared/CheckTypes';
+import ExpenditureLineGraph from './components/BMDashboard/ExpenditureGraph/ExpenditureLineGraph';
+import WishList from './components/LBDashboard/WishList/WishList';
+import InteractiveMap from './components/BMDashboard/InteractiveMap/InteractiveMap';
import AttendanceNoShow from './components/AttendanceSystem/AttendanceNoShowCharts.jsx';
import LessonsLearntChart from './components/BMDashboard/LessonsLearnt/LessonsLearntChart';
import UtilizationChart from './components/BMDashboard/UtilizationChart/UtilizationChart';
@@ -228,7 +230,6 @@ import { UserRole } from './utils/enums';
import JobApplicationForm from './components/Collaboration/JobApplicationForm/JobApplicationForm';
import WriteTaskUpload from './components/EductionPortal/Tasks/WriteTaskUpload';
import IntermediateTaskList from './components/EductionPortal/IntermediateTasks/IntermediateTaskList';
-import InteractiveMap from './components/BMDashboard/InteractiveMap/InteractiveMap';
import SimpleToolChart from './components/BMDashboard/Tools/SimpleToolChart';
import SupportLogin from './components/SupportPortal/SupportLogin';
import SupportDashboard from './components/SupportPortal/SupportDashboard';
@@ -817,6 +818,11 @@ export default (
/>
+
+
{
+ let url = `${ApiUri}/project/${projectId}/cost-breakdown`;
+
+ if (fromDate || toDate) {
+ const params = new URLSearchParams();
+ if (fromDate) params.append('fromDate', fromDate);
+ if (toDate) params.append('toDate', toDate);
+ url += `?${params.toString()}`;
+ }
+
+ return httpService.get(url);
+};
+
+const createCostBreakdown = (projectId, costs) => {
+ return httpService.post(`${ApiUri}/project/cost-breakdown`, {
+ projectId,
+ costs,
+ });
+};
+
+const addCostEntry = (projectId, costEntry) => {
+ return httpService.post(`${ApiUri}/project/${projectId}/cost-breakdown/entry`, costEntry);
+};
+
+const updateCostEntry = (projectId, costId, costEntry) => {
+ return httpService.put(
+ `${ApiUri}/project/${projectId}/cost-breakdown/entry/${costId}`,
+ costEntry,
+ );
+};
+
+const getAllCostBreakdowns = () => {
+ return httpService.get(`${ApiUri}/cost-breakdowns`);
+};
+
+const deleteCostBreakdown = projectId => {
+ return httpService.delete(`${ApiUri}/project/${projectId}/cost-breakdown`);
+};
+
+export default {
+ getCostBreakdown,
+ createCostBreakdown,
+ addCostEntry,
+ updateCostEntry,
+ getAllCostBreakdowns,
+ deleteCostBreakdown,
+};
diff --git a/src/utils/URL.js b/src/utils/URL.js
index 09f477fecce..26bbdd8fb67 100644
--- a/src/utils/URL.js
+++ b/src/utils/URL.js
@@ -12,7 +12,6 @@ export const ENDPOINTS = {
USER_PAUSE: userId => `${APIEndpoint}/userProfile/${userId}/pause`,
VERIFY_PRODUCTION_IDENTITY: `${APIEndpoint}/production-identity/verify`,
UPDATE_REHIREABLE_STATUS: userId => `${APIEndpoint}/userprofile/${userId}/rehireable`,
- TOGGLE_VISIBILITY: userId => `${APIEndpoint}/userprofile/${userId}/toggleInvisibility`,
USER_PROFILE_UPDATE: `${APIEndpoint}/userprofile/update`,
ADD_BLUE_SQUARE: userId => `${APIEndpoint}/userprofile/${userId}/addInfringement`,
@@ -52,6 +51,7 @@ export const ENDPOINTS = {
USER_AUTOCOMPLETE: searchText => `${APIEndpoint}/userProfile/autocomplete/${searchText}`,
SEARCH_USER: `${APIEndpoint}/users/search`,
TOGGLE_BIO_STATUS: userId => `${APIEndpoint}/userProfile/${userId}/toggleBio`,
+ TOGGLE_VISIBILITY: userId => `${APIEndpoint}/userProfile/${userId}/toggleVisibility`,
INFO_COLLECTIONS: `${APIEndpoint}/informations`,
INFO_COLLECTION: infoId => `${APIEndpoint}/informations/${infoId}`,
@@ -156,7 +156,8 @@ export const ENDPOINTS = {
`${APIEndpoint}/reports/volunteerstats?startDate=${startDate}&endDate=${endDate}&comparisonStartDate=${comparisonStartDate ||
''}&comparisonEndDate=${comparisonEndDate || ''}`,
VOLUNTEER_TRENDS: (timeFrame, offset, customStartDate, customEndDate) =>
- `${APIEndpoint}/reports/volunteertrends?timeFrame=${timeFrame}&offset=${offset}${customStartDate ? `&customStartDate=${customStartDate}` : ''
+ `${APIEndpoint}/reports/volunteertrends?timeFrame=${timeFrame}&offset=${offset}${
+ customStartDate ? `&customStartDate=${customStartDate}` : ''
}${customEndDate ? `&customEndDate=${customEndDate}` : ''}`,
HOURS_TOTAL_ORG_SUMMARY: (startDate, endDate) =>
`${APIEndpoint}/reports/overviewsummaries/taskandprojectstats?startDate=${startDate}&endDate=${endDate}`,
@@ -167,9 +168,11 @@ export const ENDPOINTS = {
POPUP_EDITOR_BY_ID: id => `${APIEndpoint}/popupeditor/${id}`,
POPUP_EDITOR_BACKUP_BY_ID: id => `${APIEndpoint}/backup/popupeditor/${id}`,
+
+
+
TEAM_MEMBERS: teamId => `${APIEndpoint}/team/${teamId}/users`,
TEAM_BY_ID: teamId => `${APIEndpoint}/team/${teamId}`,
- APPLICANT_VOLUNTEER_RATIO: `${APIEndpoint}/applicant-volunteer-ratio`,
USER_UNREAD_TASK_NOTIFICATIONS: userId => `${APIEndpoint}/tasknotification/user/${userId}`,
// BADGE: () => `${APIEndpoint}/badge`,
BADGE: () => `${APIEndpoint}/badge/getAllBadges`,
@@ -343,7 +346,6 @@ export const ENDPOINTS = {
},
GET_ALL_FOLLOWUPS: () => `${APIEndpoint}/followup`,
-
SET_USER_FOLLOWUP: (userId, taskId) => `${APIEndpoint}/followup/${userId}/${taskId}`,
GET_PROJECT_BY_PERSON: searchName => `${APIEndpoint}/userProfile/projects/${searchName}`,
@@ -380,9 +382,9 @@ export const ENDPOINTS = {
BM_REUSABLE_TYPES: `${APIEndpoint}/bm/invtypes/reusables`,
BM_REUSABLES: `${APIEndpoint}/bm/reusables`,
BM_PURCHASE_REUSABLES: `${APIEndpoint}/bm/reusables/purchase`,
+ BM_EQUIPMENTS: `${APIEndpoint}/bm/equipments`,
BM_EQUIPMENT_TYPES: `${APIEndpoint}/bm/invtypes/equipments`,
BM_EQUIPMENT_PURCHASE: `${APIEndpoint}/bm/equipment/purchase`,
- BM_EQUIPMENT_LOGS: `${APIEndpoint}/bm/equipments/logRecords`,
BM_PROJECTS: `${APIEndpoint}/bm/projects`,
BM_PROJECTS_WITH_LOCATION: `${APIEndpoint}/bm/projects/location`,
BM_PROJECT_EXPENSE_BY_ID: projectId => `${APIEndpoint}/bm/project/${projectId}/expenses`,
@@ -432,8 +434,19 @@ export const ENDPOINTS = {
BM_TOOL_UTILIZATION_EXPORT: `${APIEndpoint}/tools/utilization/export`,
BM_EQUIPMENT_BY_ID: singleEquipmentId => `${APIEndpoint}/bm/equipment/${singleEquipmentId}`,
BM_EQUIPMENT_STATUS_UPDATE: (equipmentId) => `${APIEndpoint}/bm/equipment/${equipmentId}/status`,
- BM_EQUIPMENTS: `${APIEndpoint}/bm/equipments`,
BM_INVTYPE_TYPE: type => `${APIEndpoint}/bm/invtypes/${type}`,
+ BM_EXPENDITURE: `${APIEndpoint}/bm/expenditure`,
+ BM_PROJECT_NAMES: `${APIEndpoint}/bm/projectsNames`,
+ EXPENDITURE_PROJECT_IDS: `${APIEndpoint}/projects/with-expenditure`,
+ PROJECT_COST_BREAKDOWN: (projectId, startDate, endDate) => {
+ let url = `${APIEndpoint}/projects/${projectId || 'all'}/cost-breakdown`;
+ const params = [];
+ if (startDate) params.push(`startDate=${encodeURIComponent(startDate)}`);
+ if (endDate) params.push(`endDate=${encodeURIComponent(endDate)}`);
+ if (params.length > 0) url += `?${params.join('&')}`;
+ return url;
+ },
+
BM_ISSUE_CHART: `${APIEndpoint}/bm/issue/issue-chart`,
BM_EQUIPMENT_INVTYPE: `${APIEndpoint}/bm/invtypes/equipment`,
BM_REUSABLES_INVTYPE: `${APIEndpoint}/bm/invtypes/reusables`,