diff --git a/src/actions/bmdashboard/costBreakdownActions.js b/src/actions/bmdashboard/costBreakdownActions.js
new file mode 100644
index 0000000000..58d88dab14
--- /dev/null
+++ b/src/actions/bmdashboard/costBreakdownActions.js
@@ -0,0 +1,43 @@
+import axios from 'axios';
+import { ENDPOINTS } from '../../utils/URL';
+import {
+ FETCH_COST_BREAKDOWN_START,
+ FETCH_COST_BREAKDOWN_SUCCESS,
+ FETCH_COST_BREAKDOWN_ERROR,
+ FETCH_COST_DETAIL_START,
+ FETCH_COST_DETAIL_SUCCESS,
+ FETCH_COST_DETAIL_ERROR,
+ CLEAR_COST_DETAIL,
+} from '../../constants/bmdashboard/costBreakdownConstants';
+
+export const fetchCostBreakdown = ({ projectId, startDate, endDate } = {}) => {
+ return async dispatch => {
+ dispatch({ type: FETCH_COST_BREAKDOWN_START });
+ try {
+ const url = ENDPOINTS.BM_COST_BREAKDOWN(projectId, startDate, endDate);
+ const res = await axios.get(url);
+ dispatch({ type: FETCH_COST_BREAKDOWN_SUCCESS, payload: res.data });
+ } catch (error) {
+ const msg = error.response?.data?.error || 'Failed to load cost breakdown';
+ dispatch({ type: FETCH_COST_BREAKDOWN_ERROR, payload: msg });
+ }
+ };
+};
+
+export const fetchCostDetail = ({ projectId, startDate, endDate } = {}) => {
+ return async dispatch => {
+ dispatch({ type: FETCH_COST_DETAIL_START });
+ try {
+ const url = ENDPOINTS.BM_COST_BREAKDOWN(projectId, startDate, endDate, true);
+ const res = await axios.get(url);
+ dispatch({ type: FETCH_COST_DETAIL_SUCCESS, payload: res.data });
+ } catch (error) {
+ const msg = error.response?.data?.error || 'Failed to load category details';
+ dispatch({ type: FETCH_COST_DETAIL_ERROR, payload: msg });
+ }
+ };
+};
+
+export const clearCostDetail = () => ({
+ type: CLEAR_COST_DETAIL,
+});
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/Financials/CostBreakDown/CostBreakDown.jsx b/src/components/BMDashboard/WeeklyProjectSummary/Financials/CostBreakDown/CostBreakDown.jsx
new file mode 100644
index 0000000000..690d160d55
--- /dev/null
+++ b/src/components/BMDashboard/WeeklyProjectSummary/Financials/CostBreakDown/CostBreakDown.jsx
@@ -0,0 +1,476 @@
+import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts';
+import axios from 'axios';
+import {
+ fetchCostBreakdown,
+ fetchCostDetail,
+ clearCostDetail,
+} from '../../../../../actions/bmdashboard/costBreakdownActions';
+import styles from './CostBreakDown.module.css';
+
+const CATEGORY_CONFIG = {
+ 'Total Cost of Labor': { label: 'Labor', color: '#9333EA' },
+ 'Total Cost of Materials': { label: 'Materials', color: '#22C55E' },
+ 'Total Cost of Equipment': { label: 'Equipment', color: '#EAB308' },
+};
+
+const DEBOUNCE_MS = 500;
+const CHAR_WIDTH_RATIO = 0.55;
+const MAX_LABEL_LINES = 2;
+
+function fitLabelInDonut(text, availableWidth, maxFontSize, minFontSize) {
+ const words = text.split(/\s+/);
+
+ for (let size = maxFontSize; size >= minFontSize; size -= 0.5) {
+ const charsPerLine = Math.floor(availableWidth / (size * CHAR_WIDTH_RATIO));
+ if (charsPerLine < 1) continue;
+
+ const lines = [];
+ let currentLine = '';
+ for (let w = 0; w < words.length; w += 1) {
+ const testLine = currentLine ? `${currentLine} ${words[w]}` : words[w];
+ if (testLine.length <= charsPerLine) {
+ currentLine = testLine;
+ } else if (!currentLine) {
+ currentLine = words[w];
+ } else {
+ lines.push(currentLine);
+ currentLine = words[w];
+ }
+ }
+ if (currentLine) lines.push(currentLine);
+
+ if (lines.length <= MAX_LABEL_LINES) {
+ return { fontSize: size, lines };
+ }
+ }
+
+ // At min size, wrap and truncate to MAX_LABEL_LINES
+ const charsPerLine = Math.max(1, Math.floor(availableWidth / (minFontSize * CHAR_WIDTH_RATIO)));
+ const lines = [];
+ let currentLine = '';
+ for (let w = 0; w < words.length; w += 1) {
+ const testLine = currentLine ? `${currentLine} ${words[w]}` : words[w];
+ if (testLine.length <= charsPerLine) {
+ currentLine = testLine;
+ } else if (!currentLine) {
+ currentLine = words[w];
+ } else {
+ lines.push(currentLine);
+ currentLine = words[w];
+ }
+ }
+ if (currentLine) lines.push(currentLine);
+
+ const truncated = lines.slice(0, MAX_LABEL_LINES);
+ if (lines.length > MAX_LABEL_LINES) {
+ const last = truncated[MAX_LABEL_LINES - 1];
+ truncated[MAX_LABEL_LINES - 1] =
+ last.length > charsPerLine - 1 ? `${last.slice(0, charsPerLine - 1)}\u2026` : `${last}\u2026`;
+ }
+ return { fontSize: minFontSize, lines: truncated };
+}
+
+function formatCurrency(amount) {
+ if (amount === null || amount === undefined || Number.isNaN(Number(amount))) return '$0';
+ try {
+ return new Intl.NumberFormat(undefined, {
+ style: 'currency',
+ currency: 'USD',
+ maximumFractionDigits: 0,
+ }).format(Number(amount));
+ } catch (e) {
+ return `$${Number(amount).toFixed(0)}`;
+ }
+}
+
+function CustomTooltip({ active, payload, totalCost, darkMode }) {
+ if (!active || !payload?.length) return null;
+ const entry = payload[0].payload;
+ const config = CATEGORY_CONFIG[entry.name] || {};
+ const percentage = totalCost > 0 ? ((entry.value / totalCost) * 100).toFixed(1) : '0.0';
+
+ return (
+
+
{config.label || entry.name}
+
+ Amount: {formatCurrency(entry.value)}
+
+
+ Share: {percentage}%
+
+
+ );
+}
+
+export default function CostBreakDown() {
+ const dispatch = useDispatch();
+
+ const { loading, data, error, detailLoading, detailData, detailError } = useSelector(
+ state => state.costBreakdown,
+ );
+ const darkMode = useSelector(state => state.theme.darkMode);
+
+ const [projects, setProjects] = useState([]);
+ const [projectId, setProjectId] = useState('');
+ const [startDate, setStartDate] = useState('');
+ const [endDate, setEndDate] = useState('');
+ const [activeIndex, setActiveIndex] = useState(null);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
+
+ const debounceRef = useRef(null);
+
+ // Load project list for dropdown
+ useEffect(() => {
+ let cancelled = false;
+ const apiBase = (process.env.REACT_APP_APIENDPOINT || '').replace(/\/$/, '');
+ axios
+ .get(`${apiBase}/bm/projectsNames`)
+ .then(res => {
+ if (cancelled) return;
+ if (Array.isArray(res.data)) {
+ const list = res.data
+ .filter(p => p?.projectId)
+ .map(p => ({ id: p.projectId, name: p.projectName || p.projectId }));
+ setProjects(list);
+ }
+ })
+ .catch(() => {
+ // Silently fail — dropdown shows only "All Projects"
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ // Fetch cost breakdown with debouncing
+ useEffect(() => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => {
+ dispatch(fetchCostBreakdown({ projectId, startDate, endDate }));
+ }, DEBOUNCE_MS);
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ };
+ }, [dispatch, projectId, startDate, endDate]);
+
+ // Window resize listener
+ useEffect(() => {
+ const handleResize = () => setWindowWidth(window.innerWidth);
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ const chartData = useMemo(() => {
+ if (!data?.breakdown?.length) return [];
+ return data.breakdown.map(item => ({
+ name: item.category,
+ shortName: (CATEGORY_CONFIG[item.category] || {}).label || item.category,
+ value: item.amount,
+ color: (CATEGORY_CONFIG[item.category] || {}).color || '#999',
+ }));
+ }, [data]);
+
+ const totalCost = data?.totalCost || 0;
+ const projectLabel = data?.project || 'All Projects';
+
+ const isXS = windowWidth <= 480;
+ let innerRadius;
+ let outerRadius;
+ let chartHeight;
+ if (isXS) {
+ innerRadius = 50;
+ outerRadius = 75;
+ chartHeight = 250;
+ } else if (windowWidth <= 768) {
+ innerRadius = 60;
+ outerRadius = 90;
+ chartHeight = 280;
+ } else {
+ innerRadius = 70;
+ outerRadius = 110;
+ chartHeight = 320;
+ }
+
+ const { fontSize: labelFontSize, lines: labelLines } = useMemo(() => {
+ const maxSize = isXS ? 12 : 14;
+ const availableWidth = innerRadius * 2 * 0.75;
+ return fitLabelInDonut(projectLabel, availableWidth, maxSize, 7);
+ }, [projectLabel, innerRadius, isXS]);
+
+ const handleSliceClick = useCallback(
+ entry => {
+ const category = entry.name;
+ if (selectedCategory === category) {
+ setSelectedCategory(null);
+ dispatch(clearCostDetail());
+ return;
+ }
+ setSelectedCategory(category);
+ dispatch(fetchCostDetail({ projectId, startDate, endDate }));
+ },
+ [dispatch, projectId, startDate, endDate, selectedCategory],
+ );
+
+ const handleFilterChange = useCallback(
+ (setter, value) => {
+ setter(value);
+ setSelectedCategory(null);
+ dispatch(clearCostDetail());
+ },
+ [dispatch],
+ );
+
+ const hasActiveFilters = projectId || startDate || endDate;
+
+ const handleClearFilters = useCallback(() => {
+ setProjectId('');
+ setStartDate('');
+ setEndDate('');
+ setSelectedCategory(null);
+ dispatch(clearCostDetail());
+ }, [dispatch]);
+
+ const renderProjectBreakdown = () => {
+ const categoryData = detailData?.breakdown?.find(b => b.category === selectedCategory);
+ if (!categoryData?.projectBreakdown?.length) {
+ return No project breakdown available
;
+ }
+ return (
+
+ {categoryData.projectBreakdown.map(proj => (
+
+ {proj.projectName}
+ {formatCurrency(proj.amount)}
+ {proj.percentage.toFixed(1)}%
+
+ ))}
+
+ );
+ };
+
+ const wrapperClass = `${styles.wrapper} ${darkMode ? styles.wrapperDark : ''}`;
+
+ return (
+
+
Cost Breakdown by Category
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+ handleFilterChange(setStartDate, e.target.value)}
+ className={styles.filterInput}
+ />
+
+
+
+
+ handleFilterChange(setEndDate, e.target.value)}
+ className={styles.filterInput}
+ />
+
+
+ {hasActiveFilters && (
+
+ )}
+
+
+ {/* Loading */}
+ {loading && (
+
+
+
Loading cost data...
+
+ )}
+
+ {/* Error */}
+ {!loading && error && (
+
+
{error}
+
+
+ )}
+
+ {/* Empty state */}
+ {!loading && !error && data && chartData.length === 0 && (
+
+
No cost data available
+
+ {projectId
+ ? 'Try selecting a different project or date range'
+ : 'No cost records found'}
+
+
+ )}
+
+ {/* Chart */}
+ {!loading && !error && chartData.length > 0 && (
+ <>
+
+
+
+ setActiveIndex(i)}
+ onMouseLeave={() => setActiveIndex(null)}
+ onClick={(_, i) => handleSliceClick(chartData[i])}
+ isAnimationActive={false}
+ >
+ {chartData.map((entry, i) => (
+ |
+ ))}
+
+
+ {/* Center label: project name */}
+ 1 ? '40%' : '46%'}
+ textAnchor="middle"
+ dominantBaseline="middle"
+ fill={darkMode ? '#f8fafc' : '#0f172a'}
+ fontSize={labelFontSize}
+ fontWeight="600"
+ >
+ {labelLines.map((line, idx) => (
+
+ {line}
+
+ ))}
+
+
+ {/* Center label: total cost */}
+ 1 ? '57%' : '55%'}
+ textAnchor="middle"
+ dominantBaseline="middle"
+ fill={darkMode ? '#f8fafc' : '#0f172a'}
+ fontSize={isXS ? 16 : 20}
+ fontWeight="800"
+ >
+ {formatCurrency(totalCost)}
+
+
+ }
+ cursor={false}
+ allowEscapeViewBox={{ x: false, y: false }}
+ />
+
+
+
+
+ {/* Legend */}
+
+ {chartData.map(entry => (
+
+ ))}
+
+
+ {/* Detail panel */}
+ {selectedCategory && (
+
+
+
+ {(CATEGORY_CONFIG[selectedCategory] || {}).label || selectedCategory} by Project
+
+
+
+
+ {detailLoading &&
Loading details...
}
+ {!detailLoading && detailError && (
+
{detailError}
+ )}
+ {!detailLoading && !detailError && detailData && renderProjectBreakdown()}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/Financials/CostBreakDown/CostBreakDown.module.css b/src/components/BMDashboard/WeeklyProjectSummary/Financials/CostBreakDown/CostBreakDown.module.css
new file mode 100644
index 0000000000..f0eab09e5e
--- /dev/null
+++ b/src/components/BMDashboard/WeeklyProjectSummary/Financials/CostBreakDown/CostBreakDown.module.css
@@ -0,0 +1,430 @@
+/* ===================== Wrapper ===================== */
+.wrapper {
+ --cb-text-color: #000;
+ --cb-input-bg: #fff;
+ --cb-input-border: #d1d5db;
+ --cb-tooltip-bg: #fff;
+ --cb-tooltip-border: #e5e7eb;
+ --cb-detail-bg: #f9fafb;
+ width: 100%;
+ height: 100%;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+.wrapperDark {
+ --cb-text-color: #fff;
+ --cb-input-bg: #2b3e59;
+ --cb-input-border: #4a5a77;
+ --cb-tooltip-bg: #253342;
+ --cb-tooltip-border: #4a5a77;
+ --cb-detail-bg: #253342;
+ background: #2b3e59;
+}
+
+/* ===================== Title ===================== */
+.title {
+ text-align: center;
+ font-weight: 600;
+ margin: 0 0 1rem;
+ font-size: 1.1rem;
+ color: var(--cb-text-color);
+}
+
+/* ===================== Filters ===================== */
+.filters {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.filterItem {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ font-size: 14px;
+ min-width: 140px;
+}
+
+.filterLabel {
+ font-weight: 600;
+ font-size: 13px;
+ color: var(--cb-text-color);
+ margin-bottom: 4px;
+}
+
+.filterSelect,
+.filterInput {
+ width: 100%;
+ padding: 6px 10px;
+ font-size: 14px;
+ border: 1px solid var(--cb-input-border);
+ border-radius: 6px;
+ background: var(--cb-input-bg);
+ color: var(--cb-text-color);
+ transition: border-color 0.15s ease;
+}
+
+.filterSelect:focus,
+.filterInput:focus {
+ outline: none;
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
+}
+
+.filterSelect:hover,
+.filterInput:hover {
+ border-color: #9ca3af;
+}
+
+.clearFiltersButton {
+ align-self: flex-end;
+ padding: 6px 14px;
+ font-size: 13px;
+ font-weight: 500;
+ border: 1px solid var(--cb-input-border);
+ border-radius: 6px;
+ background: transparent;
+ color: var(--cb-text-color);
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.clearFiltersButton:hover {
+ background: rgba(0, 0, 0, 0.05);
+ border-color: #9ca3af;
+}
+
+.wrapperDark .clearFiltersButton:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+/* ===================== Chart Container ===================== */
+.chartContainer {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+}
+
+/* ===================== Tooltip ===================== */
+.tooltip {
+ background: var(--cb-tooltip-bg);
+ border: 1px solid var(--cb-tooltip-border);
+ border-radius: 8px;
+ padding: 10px 14px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+ min-width: 140px;
+}
+
+.tooltipTitle {
+ font-weight: 700;
+ font-size: 14px;
+ margin-bottom: 6px;
+ color: var(--cb-text-color);
+}
+
+.tooltipRow {
+ font-size: 13px;
+ color: var(--cb-text-color);
+ margin-bottom: 2px;
+}
+
+/* ===================== Legend ===================== */
+.legend {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-top: 1rem;
+}
+
+.legendItem {
+ color: #fff;
+ padding: 6px 20px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ min-width: 120px;
+ text-align: center;
+ border: 2px solid transparent;
+ cursor: pointer;
+ transition: opacity 0.15s ease, border-color 0.15s ease;
+}
+
+.legendItem:hover {
+ opacity: 0.85;
+}
+
+.legendItemActive {
+ border-color: var(--cb-text-color);
+}
+
+/* ===================== Detail Panel ===================== */
+.detailPanel {
+ margin-top: 1rem;
+ padding: 16px;
+ background: var(--cb-detail-bg);
+ border-radius: 8px;
+ border: 1px solid var(--cb-input-border);
+}
+
+.detailHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.detailTitle {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--cb-text-color);
+ margin: 0;
+}
+
+.detailClose {
+ background: none;
+ border: 1px solid var(--cb-input-border);
+ border-radius: 4px;
+ color: var(--cb-text-color);
+ font-size: 14px;
+ padding: 2px 8px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.detailClose:hover {
+ background: rgba(0, 0, 0, 0.05);
+}
+
+.wrapperDark .detailClose:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.projectList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.projectRow {
+ display: grid;
+ grid-template-columns: 1fr auto auto;
+ gap: 12px;
+ align-items: center;
+ padding: 8px 10px;
+ border-radius: 6px;
+ background: rgba(0, 0, 0, 0.03);
+ color: var(--cb-text-color);
+}
+
+.wrapperDark .projectRow {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.projectName {
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.projectAmount {
+ font-size: 14px;
+ font-weight: 600;
+ text-align: right;
+}
+
+.projectPct {
+ font-size: 13px;
+ color: #6b7280;
+ min-width: 50px;
+ text-align: right;
+}
+
+.wrapperDark .projectPct {
+ color: #9ca3af;
+}
+
+/* ===================== States ===================== */
+.loading,
+.empty,
+.error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ color: var(--cb-text-color);
+ text-align: center;
+ gap: 8px;
+}
+
+.spinner {
+ width: 36px;
+ height: 36px;
+ border: 3px solid var(--cb-input-border);
+ border-top-color: #3b82f6;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.emptySubtext {
+ font-size: 13px;
+ color: #6b7280;
+}
+
+.wrapperDark .emptySubtext {
+ color: #9ca3af;
+}
+
+.errorMessage {
+ color: #b91c1c;
+ font-size: 14px;
+ margin: 0;
+}
+
+.wrapperDark .errorMessage {
+ color: #fca5a5;
+}
+
+.retryButton {
+ margin-top: 8px;
+ padding: 6px 16px;
+ font-size: 13px;
+ border: 1px solid var(--cb-input-border);
+ border-radius: 6px;
+ background: var(--cb-input-bg);
+ color: var(--cb-text-color);
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.retryButton:hover {
+ background: rgba(0, 0, 0, 0.05);
+}
+
+.wrapperDark .retryButton:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.detailLoading,
+.detailError {
+ padding: 12px;
+ text-align: center;
+ font-size: 14px;
+ color: var(--cb-text-color);
+}
+
+.noDetail {
+ padding: 12px;
+ text-align: center;
+ font-size: 14px;
+ color: #6b7280;
+}
+
+.wrapperDark .noDetail {
+ color: #9ca3af;
+}
+
+/* ===================== Dark Mode: Native Widget Colors ===================== */
+.wrapperDark .filterSelect,
+.wrapperDark .filterInput {
+ color-scheme: dark;
+}
+
+.wrapperDark .filterInput[type='date']::-webkit-calendar-picker-indicator {
+ filter: invert(1) opacity(0.8);
+}
+
+/* ===================== Responsive ===================== */
+@media (max-width: 768px) {
+ .filters {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0.75rem;
+ }
+
+ .filterItem {
+ width: 100%;
+ min-width: auto;
+ }
+
+ .wrapper {
+ padding: 14px;
+ }
+
+ .projectRow {
+ grid-template-columns: 1fr;
+ gap: 4px;
+ }
+
+ .projectAmount,
+ .projectPct {
+ text-align: left;
+ }
+}
+
+@media (max-width: 480px) {
+ .title {
+ font-size: 0.95rem;
+ }
+
+ .legend {
+ gap: 0.5rem;
+ }
+
+ .legendItem {
+ min-width: 90px;
+ padding: 5px 12px;
+ font-size: 13px;
+ }
+}
+
+/* ===================== Accessibility ===================== */
+@media (prefers-reduced-motion: reduce) {
+ .spinner {
+ animation: none;
+ }
+
+ .legendItem,
+ .filterSelect,
+ .filterInput,
+ .clearFiltersButton,
+ .detailClose,
+ .retryButton {
+ transition: none;
+ }
+}
+
+@media (prefers-contrast: high) {
+ .wrapper {
+ border: 2px solid #000;
+ }
+
+ .filterSelect,
+ .filterInput {
+ border: 2px solid #000;
+ }
+}
+
+@media print {
+ .filters {
+ display: none;
+ }
+
+ .detailClose {
+ display: none;
+ }
+}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
index a2b7aec91f..a61d05b02e 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx
@@ -12,6 +12,7 @@ import InjuryCategoryBarChart from './GroupedBarGraphInjurySeverity/InjuryCatego
import ToolsHorizontalBarChart from './Tools/ToolsHorizontalBarChart';
import ExpenseBarChart from './Financials/ExpenseBarChart';
import FinancialStatButtons from './Financials/FinancialStatButtons';
+import CostBreakDown from './Financials/CostBreakDown/CostBreakDown';
import ActualVsPlannedCost from './ActualVsPlannedCost/ActualVsPlannedCost';
import TotalMaterialCostPerProject from './TotalMaterialCostPerProject/TotalMaterialCostPerProject';
import styles from './WeeklyProjectSummary.module.css';
@@ -379,7 +380,9 @@ function WeeklyProjectSummary() {
- 📊 Big Card
+
+
+
),
},
diff --git a/src/constants/bmdashboard/costBreakdownConstants.js b/src/constants/bmdashboard/costBreakdownConstants.js
new file mode 100644
index 0000000000..6eccca9da3
--- /dev/null
+++ b/src/constants/bmdashboard/costBreakdownConstants.js
@@ -0,0 +1,9 @@
+export const FETCH_COST_BREAKDOWN_START = 'FETCH_COST_BREAKDOWN_START';
+export const FETCH_COST_BREAKDOWN_SUCCESS = 'FETCH_COST_BREAKDOWN_SUCCESS';
+export const FETCH_COST_BREAKDOWN_ERROR = 'FETCH_COST_BREAKDOWN_ERROR';
+
+export const FETCH_COST_DETAIL_START = 'FETCH_COST_DETAIL_START';
+export const FETCH_COST_DETAIL_SUCCESS = 'FETCH_COST_DETAIL_SUCCESS';
+export const FETCH_COST_DETAIL_ERROR = 'FETCH_COST_DETAIL_ERROR';
+
+export const CLEAR_COST_DETAIL = 'CLEAR_COST_DETAIL';
diff --git a/src/reducers/bmdashboard/costBreakdownReducer.js b/src/reducers/bmdashboard/costBreakdownReducer.js
new file mode 100644
index 0000000000..bbbca2f02b
--- /dev/null
+++ b/src/reducers/bmdashboard/costBreakdownReducer.js
@@ -0,0 +1,46 @@
+import {
+ FETCH_COST_BREAKDOWN_START,
+ FETCH_COST_BREAKDOWN_SUCCESS,
+ FETCH_COST_BREAKDOWN_ERROR,
+ FETCH_COST_DETAIL_START,
+ FETCH_COST_DETAIL_SUCCESS,
+ FETCH_COST_DETAIL_ERROR,
+ CLEAR_COST_DETAIL,
+} from '../../constants/bmdashboard/costBreakdownConstants';
+
+const defaultState = {
+ loading: false,
+ data: null,
+ error: null,
+
+ detailLoading: false,
+ detailData: null,
+ detailError: null,
+};
+
+// eslint-disable-next-line default-param-last
+export const costBreakdownReducer = (state = defaultState, action) => {
+ switch (action.type) {
+ case FETCH_COST_BREAKDOWN_START:
+ return { ...state, loading: true, error: null };
+ case FETCH_COST_BREAKDOWN_SUCCESS:
+ return { ...state, loading: false, data: action.payload, error: null };
+ case FETCH_COST_BREAKDOWN_ERROR:
+ return { ...state, loading: false, data: null, error: action.payload };
+
+ case FETCH_COST_DETAIL_START:
+ return { ...state, detailLoading: true, detailError: null };
+ case FETCH_COST_DETAIL_SUCCESS:
+ return { ...state, detailLoading: false, detailData: action.payload, detailError: null };
+ case FETCH_COST_DETAIL_ERROR:
+ return { ...state, detailLoading: false, detailData: null, detailError: action.payload };
+
+ case CLEAR_COST_DETAIL:
+ return { ...state, detailLoading: false, detailData: null, detailError: null };
+
+ default:
+ return state;
+ }
+};
+
+export default costBreakdownReducer;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index ca43404639..ec7ff0dc86 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -63,6 +63,7 @@ import { equipmentReducer } from './bmdashboard/equipmentReducer';
import { bmProjectMemberReducer } from './bmdashboard/projectMemberReducer';
import { bmTimeLoggerReducer } from './bmdashboard/timeLoggerReducer';
import bmInjuryReducer from './bmdashboard/injuryReducer';
+import { costBreakdownReducer } from './bmdashboard/costBreakdownReducer';
import dashboardReducer from './dashboardReducer';
import { timeOffRequestsReducer } from './timeOffRequestReducer';
@@ -165,6 +166,7 @@ const localReducers = {
dashboard: dashboardReducer,
injuries: injuriesReducer,
weeklyProjectSummary: weeklyProjectSummaryReducer,
+ costBreakdown: costBreakdownReducer,
// lbdashboard
wishlistItem: wishListReducer,
diff --git a/src/utils/URL.js b/src/utils/URL.js
index 3124b4bd04..265b8dc6bf 100644
--- a/src/utils/URL.js
+++ b/src/utils/URL.js
@@ -346,6 +346,16 @@ export const ENDPOINTS = {
BM_TOOLS_RETURNED_LATE: `${APIEndpoint}/bm/tools/returned-late`,
BM_TOOLS_RETURNED_LATE_PROJECTS: `${APIEndpoint}/bm/tools/returned-late/projects`,
TOOLS_AVAILABILITY_PROJECTS: `${APIEndpoint}/bm/tools-availability/projects`,
+ BM_COST_BREAKDOWN: (projectId, startDate, endDate, categoryDetail) => {
+ let url = `${APIEndpoint}/costs/breakdown`;
+ const params = [];
+ if (projectId) params.push(`projectId=${projectId}`);
+ if (startDate) params.push(`startDate=${startDate}`);
+ if (endDate) params.push(`endDate=${endDate}`);
+ if (categoryDetail) params.push('categoryDetail=true');
+ if (params.length > 0) url += `?${params.join('&')}`;
+ return url;
+ },
TOOLS_AVAILABILITY_BY_PROJECT: (projectId, startDate, endDate) => {
let url = `${APIEndpoint}/bm/projects/${projectId}/tools-availability`;
const params = [];