diff --git a/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx b/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx
new file mode 100644
index 0000000000..fe4669390a
--- /dev/null
+++ b/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx
@@ -0,0 +1,639 @@
+import { useState, useEffect, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+ ReferenceLine,
+} from 'recharts';
+import moment from 'moment';
+import Select from 'react-select';
+import { getProjectCosts, getProjectIds } from '../../../services/projectCostTrackingService';
+import { useSelector } from 'react-redux';
+import ReactTooltip from 'react-tooltip';
+import { Info } from 'lucide-react';
+import styles from './CostPredictionPage.module.css';
+import {
+ DARK,
+ LIGHT,
+ getSingleSelectStyles,
+ getMultiSelectStyles,
+ getAxisTickFill,
+ getGridStroke,
+} from '../../../utils/bmChartStyles';
+
+// Cost category options
+const costOptions = [
+ { value: 'Labor', label: 'Labor Cost' },
+ { value: 'Materials', label: 'Materials Cost' },
+ { value: 'Equipment', label: 'Equipment Cost' },
+ { value: 'Total', label: 'Total Cost' },
+];
+
+// Custom dot component for predicted values - consolidated to reduce duplication
+function PredictedDot({ cx, cy, payload, category, costColors, size = 4 }) {
+ if (!payload || !payload[`${category}Predicted`]) return null;
+ return (
+
+ );
+}
+
+PredictedDot.propTypes = {
+ cx: PropTypes.number,
+ cy: PropTypes.number,
+ payload: PropTypes.object,
+ category: PropTypes.string.isRequired,
+ costColors: PropTypes.object.isRequired,
+ size: PropTypes.number,
+};
+
+// Define line colors
+const costColors = {
+ Labor: '#4589FF',
+ Materials: '#FF6A00',
+ Equipment: '#8A2BE2',
+ Total: '#3CB371',
+};
+
+// Create specific dot components for common categories
+function createDotComponent(category) {
+ function DotComponent(props) {
+ return (
+
+ );
+ }
+ DotComponent.propTypes = {
+ cx: PropTypes.number,
+ cy: PropTypes.number,
+ payload: PropTypes.object,
+ };
+ return DotComponent;
+}
+
+// Pre-defined dot components for different categories
+const LaborDot = createDotComponent('Labor');
+const MaterialsDot = createDotComponent('Materials');
+const EquipmentDot = createDotComponent('Equipment');
+const TotalDot = createDotComponent('Total');
+
+// Calculate last predicted values for reference lines
+const getLastPredictedValues = costData => {
+ const lastValues = {};
+
+ if (!costData || !costData.predicted) {
+ return lastValues;
+ }
+
+ Object.keys(costData.predicted).forEach(category => {
+ const predictedItems = costData.predicted[category];
+ if (predictedItems && predictedItems.length > 0) {
+ // Get the last predicted value for this category
+ const lastPredicted = predictedItems[predictedItems.length - 1];
+ lastValues[category] = lastPredicted.cost;
+ }
+ });
+
+ return lastValues;
+};
+
+// Process data for chart - modify this function to connect actual and predicted data
+const processDataForChart = costData => {
+ const processedData = [];
+
+ if (!costData || !costData.actual) {
+ return processedData;
+ }
+
+ // Get all dates from both actual and predicted data
+ const allDates = new Set();
+ const actualCategories = Object.keys(costData.actual);
+ const predictedCategories = costData.predicted ? Object.keys(costData.predicted) : [];
+
+ // Collect all dates
+ actualCategories.forEach(category => {
+ costData.actual[category].forEach(costItem => {
+ const dateStr = moment(costItem.date).format('MMM YYYY');
+ allDates.add(dateStr);
+ });
+ });
+
+ if (costData.predicted) {
+ predictedCategories.forEach(category => {
+ costData.predicted[category].forEach(costItem => {
+ const dateStr = moment(costItem.date).format('MMM YYYY');
+ allDates.add(dateStr);
+ });
+ });
+ }
+
+ // Sort dates chronologically
+ const sortedDates = Array.from(allDates).sort((a, b) =>
+ moment(a, 'MMM YYYY').diff(moment(b, 'MMM YYYY')),
+ );
+
+ // Create data points for each date
+ sortedDates.forEach(dateStr => {
+ const dataPoint = { date: dateStr };
+
+ // Add actual data values
+ actualCategories.forEach(category => {
+ const costItem = costData.actual[category].find(
+ costEntry => moment(costEntry.date).format('MMM YYYY') === dateStr,
+ );
+ if (costItem) {
+ dataPoint[category] = costItem.cost;
+ }
+ });
+
+ // Add predicted data values
+ if (costData.predicted) {
+ predictedCategories.forEach(category => {
+ const costItem = costData.predicted[category].find(
+ costEntry => moment(costEntry.date).format('MMM YYYY') === dateStr,
+ );
+ if (costItem) {
+ dataPoint[`${category}Predicted`] = costItem.cost;
+ }
+ });
+ }
+
+ // For the last actual data point of each category, also add it as the first predicted point
+ if (costData.predicted) {
+ actualCategories.forEach(category => {
+ const actualItems = costData.actual[category];
+ if (actualItems && actualItems.length > 0) {
+ const lastActualItem = actualItems[actualItems.length - 1];
+ const lastActualDateStr = moment(lastActualItem.date).format('MMM YYYY');
+
+ if (dateStr === lastActualDateStr) {
+ dataPoint[`${category}Predicted`] = lastActualItem.cost;
+ }
+ }
+ });
+ }
+
+ processedData.push(dataPoint);
+ });
+
+ return processedData;
+};
+
+// Shared marker component for legend and tooltip (diamond for predicted, circle for actual)
+function ChartMarker({ color, isPredicted }) {
+ return isPredicted ? (
+
+ ) : (
+
+ );
+}
+
+ChartMarker.propTypes = {
+ color: PropTypes.string.isRequired,
+ isPredicted: PropTypes.bool.isRequired,
+};
+
+// Map dataKey base names to display labels
+const COST_LABELS = {
+ Labor: 'Labor Cost',
+ Materials: 'Materials Cost',
+ Equipment: 'Equipment Cost',
+ Total: 'Total Cost',
+};
+
+// Custom tooltip component
+function CustomTooltip({ active, payload, label, currency, darkMode }) {
+ if (!active || !payload || !payload.length) {
+ return null;
+ }
+
+ const textColor = darkMode ? DARK.text : LIGHT.text;
+
+ // Check if any payload entry is predicted data
+ const hasActualData = payload.some(entry => !entry.dataKey.includes('Predicted'));
+ const hasPredictedData = payload.some(entry => entry.dataKey.includes('Predicted'));
+
+ // If both actual and predicted exist, prioritize showing "Actual"
+ const displayType = hasActualData ? 'Actual' : hasPredictedData ? 'Predicted' : 'Actual';
+ // Filter payload: if actual data exists, only show actual data; otherwise show predicted data
+ const filteredPayload = hasActualData
+ ? payload.filter(entry => !entry.dataKey.includes('Predicted'))
+ : payload;
+
+ return (
+
+
+ {label}
+
+
+ {displayType}
+
+ {filteredPayload.map(entry => {
+ const isPredicted = entry.dataKey.includes('Predicted');
+ const baseDataKey = entry.dataKey.replace('Predicted', '');
+ const costLabel = COST_LABELS[baseDataKey] || baseDataKey;
+
+ return (
+
+
+
+ {costLabel}:
+
+
+ {`${currency}${entry.value.toLocaleString()}`}
+
+
+ );
+ })}
+
+ );
+}
+
+CustomTooltip.propTypes = {
+ active: PropTypes.bool,
+ payload: PropTypes.array,
+ label: PropTypes.string,
+ currency: PropTypes.string.isRequired,
+ darkMode: PropTypes.bool,
+};
+
+function CostPredictionPage({ projectId }) {
+ const [data, setData] = useState([]);
+ const [selectedCosts, setSelectedCosts] = useState(['Labor', 'Materials']);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [currency] = useState('$');
+ const [availableProjects, setAvailableProjects] = useState([]);
+ const [selectedProject, setSelectedProject] = useState(null);
+ const [lastPredictedValues, setLastPredictedValues] = useState({});
+ const darkMode = useSelector(state => state.theme.darkMode);
+
+ useEffect(() => {
+ const fetchProjects = async () => {
+ try {
+ const projectIds = await getProjectIds();
+ setAvailableProjects(projectIds.map(id => ({ value: id, label: id })));
+ if (projectIds.length > 0) {
+ const initialProject = projectId || projectIds[0];
+ setSelectedProject({ value: initialProject, label: initialProject });
+ }
+ } catch {
+ setError('Failed to load projects');
+ }
+ };
+ fetchProjects();
+ }, [projectId]);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ if (!selectedProject) return;
+ setLoading(true);
+ try {
+ const costData = await getProjectCosts(selectedProject.value);
+ setData(processDataForChart(costData));
+ setLastPredictedValues(getLastPredictedValues(costData));
+ setLoading(false);
+ } catch {
+ setError('Failed to load cost data');
+ setLoading(false);
+ }
+ };
+ fetchData();
+ }, [selectedProject]);
+
+ const handleCostChange = selected => {
+ const selectedValues = selected ? selected.map(option => option.value) : [];
+ setSelectedCosts(selectedValues);
+ };
+ const handleProjectChange = selected => setSelectedProject(selected);
+
+ // Pick the right dot component by name
+ const getDotRenderer = category => {
+ switch (category) {
+ case 'Labor':
+ return LaborDot;
+ case 'Materials':
+ return MaterialsDot;
+ case 'Equipment':
+ return EquipmentDot;
+ case 'Total':
+ return TotalDot;
+ default:
+ return LaborDot;
+ }
+ };
+
+ // Toggle dark-mode body class for global Recharts overrides in the CSS module
+ useEffect(() => {
+ document.body.classList.toggle('dark-mode-body', darkMode);
+ return () => document.body.classList.remove('dark-mode-body');
+ }, [darkMode]);
+
+ return (
+
+ {/* ReactTooltip moved outside wrapper for better positioning */}
+
+ Chart Overview
+ This chart compares planned vs actual costs across different categories.
+
+ - Solid lines represent actual costs for each category.
+ - Dashed lines with diamond markers represent predicted/planned costs.
+ - Hover over lines to view exact cost values.
+ -
+ The dropdown filters allow you to:
+
+ - Select specific cost categories (multi-select).
+ - Pick a specific project.
+
+
+ -
+ Color coding:
+
+ -
+ Blue β Labor costs
+
+ -
+ Orange β Materials costs
+
+ -
+ Purple β Equipment costs
+
+ -
+ Green β Total costs
+
+
+
+
+
+
+
+
+
Planned v Actual Costs Tracking
+
+
+
+
+
+
+
+
+
+ {loading &&
Loading...
}
+ {error &&
{error}
}
+
+ {!loading && !error && data.length > 0 && (
+
+
+
+
+
+ `${currency}${value}`}
+ width={50}
+ />
+ } />
+
+
+
+ )}
+
+ {!loading && !error && data.length === 0 && (
+
+ )}
+
+
+ {/* Fixed label below chart */}
+
+ π Actual Costs
+ vs
+ π Predicted Costs
+
+
+
+ );
+}
+
+CostPredictionPage.propTypes = {
+ projectId: PropTypes.string,
+};
+
+export default CostPredictionPage;
diff --git a/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css b/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css
new file mode 100644
index 0000000000..b9caaede0f
--- /dev/null
+++ b/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css
@@ -0,0 +1,247 @@
+/* Cost Prediction Page Styles */
+.costPredictionPage {
+ padding: 20px;
+ background-color: var(--card-bg);
+ min-height: 100vh;
+ color: var(--text-color);
+}
+
+/* Chart Title Container */
+.chartTitleContainer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 15px;
+ padding: 0 10px;
+ position: relative;
+}
+
+.costPredictionTitle {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-color);
+ text-align: center;
+}
+
+.costPredictionInfoButton {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 4px;
+ color: var(--text-color);
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+ position: absolute;
+ right: 10px;
+}
+
+.costPredictionInfoButton:hover {
+ opacity: 1;
+}
+
+.costPredictionTooltip {
+ max-width: 300px;
+ font-size: 12px;
+ line-height: 1.4;
+ z-index: 1000 !important;
+ position: fixed !important;
+}
+
+/* Dropdown Container */
+.dropdownContainer {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+ padding: 0 10px;
+ flex-wrap: wrap;
+}
+
+.dropdownItem {
+ flex: 1;
+ min-width: 120px;
+}
+
+.multiSelect {
+ min-width: 150px;
+}
+
+/* Chart Wrapper - protects from external styles */
+.costPredictionWrapper {
+ width: 100%;
+ height: 100%;
+ padding: 10px;
+ box-sizing: border-box;
+ position: relative;
+ background-color: var(--card-bg);
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* Chart Container */
+.costPredictionChartContainer {
+ position: relative;
+ height: 500px;
+ width: 100%;
+ padding: 0 10px;
+ margin-bottom: 5px;
+ background-color: transparent;
+}
+
+/* Chart Legend */
+.chartLegend {
+ width: 100%;
+ text-align: center;
+ font-size: 14px;
+ margin-top: 2px;
+ color: var(--text-color);
+ display: flex;
+ justify-content: center;
+ gap: 6px;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.costChartLoading,
+.costChartError,
+.costChartEmpty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: var(--text-color);
+ font-size: 16px;
+}
+
+.costChartError {
+ color: #ff4d4f;
+}
+
+/* Chart Tooltip Styling */
+.costPredictionChartContainer :global(.recharts-tooltip-wrapper) {
+ z-index: 1000 !important;
+}
+
+.costPredictionChartContainer :global(.recharts-default-tooltip) {
+ background-color: var(--card-bg) !important;
+ border: 1px solid var(--button-hover) !important;
+ border-radius: 4px !important;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
+ padding: 8px !important;
+}
+
+.costPredictionChartContainer :global(.recharts-tooltip-label) {
+ color: var(--text-color) !important;
+ font-weight: bold !important;
+ margin-bottom: 4px !important;
+}
+
+.costPredictionChartContainer :global(.recharts-tooltip-item) {
+ color: var(--text-color) !important;
+ padding: 2px 0 !important;
+}
+
+.costPredictionChartContainer :global(.recharts-tooltip-item-name) {
+ color: var(--text-color) !important;
+}
+
+.costPredictionChartContainer :global(.recharts-tooltip-item-value) {
+ color: var(--text-color) !important;
+ font-weight: bold !important;
+}
+
+/* Dark mode styles */
+:global(.dark-mode) .costPredictionPage {
+ background-color: #1e2736;
+ color: #e0e0e0;
+}
+
+:global(.dark-mode) .costPredictionWrapper {
+ background-color: #2c3344;
+}
+
+:global(.dark-mode) .costPredictionChartContainer {
+ background-color: #1e2736;
+ color: #e0e0e0;
+}
+
+:global(.dark-mode) .costPredictionChartContainer :global(.recharts-default-tooltip) {
+ background-color: #2c3344 !important;
+ border-color: #364156 !important;
+ color: #e0e0e0 !important;
+}
+
+:global(.dark-mode) .costPredictionTitle {
+ color: #e0e0e0;
+}
+
+:global(.dark-mode) .costPredictionInfoButton {
+ color: #e0e0e0;
+}
+
+:global(.dark-mode) .costPredictionInfoButton:hover {
+ color: #ffffff;
+}
+
+/* Dark-mode body overrides for Recharts (replaces injected