From 9a9a33c7b092964905fc1ff23e51384a5313165e Mon Sep 17 00:00:00 2001 From: shashankm9 Date: Thu, 9 Oct 2025 22:22:39 -0500 Subject: [PATCH 1/6] BM Dashboard: restore Financials Tracking (auth via httpService), fix charts, and tidy code --- .../CostPrediction/CostPredictionPage.jsx | 722 ++++++++++++++++++ .../CostPredictionPage.module.css | 220 ++++++ .../ExpenditureChart/ExpenditureChart.jsx | 14 +- .../ExpenditureChart.module.css | 26 +- .../FinancialsTrackingCard.jsx | 7 +- .../WeeklyProjectSummary.jsx | 2 + src/routes.jsx | 2 + src/services/projectCostTrackingService.js | 33 + src/utils/URL.js | 12 + 9 files changed, 1023 insertions(+), 15 deletions(-) create mode 100644 src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx create mode 100644 src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css create mode 100644 src/services/projectCostTrackingService.js diff --git a/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx b/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx new file mode 100644 index 0000000000..a9bf9df71f --- /dev/null +++ b/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx @@ -0,0 +1,722 @@ +import { useState, useEffect, Fragment } from 'react'; +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'; + +// 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 - extracted to avoid nested component definition +function PredictedDot({ cx, cy, payload, category, costColors }) { + if (!payload || !payload[`${category}Predicted`]) return null; + return ( + + ); +} + +// Custom dot component for full page predicted values - extracted to avoid nested component definition +function FullPagePredictedDot({ cx, cy, payload, category, costColors }) { + if (!payload || !payload[`${category}Predicted`]) return null; + return ( + + ); +} + +// Define line colors +const costColors = { + Labor: '#4589FF', + Materials: '#FF6A00', + Equipment: '#8A2BE2', + Total: '#3CB371', +}; + +// Create specific dot components for common categories +function createDotComponent(category) { + return function DotComponent(props) { + return ( + + ); + }; +} + +// 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; +}; + +// Custom tooltip component +function CustomTooltip({ active, payload, label, currency }) { + if (!active || !payload || !payload.length) { + return null; + } + + // 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'); + let costLabel = ''; + const baseDataKey = entry.dataKey.replace('Predicted', ''); + + if (baseDataKey === 'Labor') costLabel = 'Labor Cost'; + else if (baseDataKey === 'Materials') costLabel = 'Materials Cost'; + else if (baseDataKey === 'Equipment') costLabel = 'Equipment Cost'; + else if (baseDataKey === 'Total') costLabel = 'Total Cost'; + + return ( +
+ {isPredicted ? ( + // Solid diamond shape for predicted data + + ) : ( + // Solid circle for actual data + + )} + + {costLabel}: + + + {`${currency}${entry.value.toLocaleString()}`} + +
+ ); + })} +
+ ); +} + +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; + } + }; + + // Apply dark mode styles to document body when in dark mode + useEffect(() => { + if (darkMode) { + document.body.classList.add('dark-mode-body'); + } else { + document.body.classList.remove('dark-mode-body'); + } + + // Add dark mode CSS for chart + if (!document.getElementById('dark-mode-styles-cost-prediction')) { + const styleElement = document.createElement('style'); + styleElement.id = 'dark-mode-styles-cost-prediction'; + styleElement.innerHTML = ` + .dark-mode-body .recharts-wrapper, + .dark-mode-body .recharts-surface { + background-color: #1e2736 !important; + } + .dark-mode-body .recharts-cartesian-grid-horizontal line, + .dark-mode-body .recharts-cartesian-grid-vertical line { + stroke: #364156 !important; + } + .dark-mode-body .recharts-text { + fill: #e0e0e0 !important; + } + .dark-mode-body .recharts-default-legend { + background-color: #1e2736 !important; + } + .dark-mode-body .recharts-tooltip-wrapper { + background-color: transparent !important; + } + .dark-mode-body .cost-prediction-page { + background-color: #1e2736 !important; + color: #e0e0e0 !important; + } + `; + document.head.appendChild(styleElement); + } + + return () => { + // Cleanup + 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

+ +
+ +
+ selectedCosts.includes(option.value))} + onChange={handleCostChange} + placeholder="All Cost Categories" + classNamePrefix="custom-select" + className={`${styles.dropdownItem} ${styles.multiSelect}`} + menuPosition="fixed" + closeMenuOnSelect={false} + hideSelectedOptions={false} + styles={ + darkMode + ? { + control: baseStyles => ({ + ...baseStyles, + backgroundColor: '#2c3344', + borderColor: '#364156', + }), + menu: baseStyles => ({ + ...baseStyles, + backgroundColor: '#2c3344', + }), + option: (baseStyles, state) => ({ + ...baseStyles, + backgroundColor: state.isFocused ? '#364156' : '#2c3344', + color: '#e0e0e0', + }), + multiValue: baseStyles => ({ + ...baseStyles, + backgroundColor: '#364156', + }), + multiValueLabel: baseStyles => ({ + ...baseStyles, + color: '#e0e0e0', + }), + multiValueRemove: baseStyles => ({ + ...baseStyles, + color: '#e0e0e0', + ':hover': { backgroundColor: '#ff4d4f', color: '#fff' }, + }), + } + : {} + } + /> +
+ +
+ {loading &&
Loading...
} + {error &&
{error}
} + + {!loading && !error && data.length > 0 && ( +
+ + + + + `${currency}${value}`} + width={50} + /> + } /> + { + const { payload } = props; + return ( +
+ {payload.map((entry, index) => { + const isPredicted = entry.value.includes('Predicted'); + return ( +
+ {isPredicted ? ( + // Solid diamond shape for predicted +
+ ) : ( + // Solid circle for actual +
+ )} + {entry.value} +
+ ); + })} +
+ ); + }} + /> + + {/* Reference Lines for Last Predicted Values */} + {(selectedCosts.length > 0 + ? selectedCosts + : ['Labor', 'Materials'] + ).map(category => + lastPredictedValues[category] ? ( + + ) : null, + )} + + {/* Dynamically render lines based on selected costs */} + {(selectedCosts.length > 0 ? selectedCosts : ['Labor', 'Materials']).map( + category => ( + + {/* Actual cost line */} + + {/* Predicted cost line */} + + + ), + )} + + +
+ )} + + {!loading && !error && data.length === 0 && ( +
+

No data available

+
+ )} +
+ + {/* Fixed label below chart */} +
+ πŸ“Š Actual Costs + vs + πŸ“ˆ Predicted Costs +
+
+
+ ); +} + +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..dc76361299 --- /dev/null +++ b/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css @@ -0,0 +1,220 @@ +/* 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: 12px; + 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; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .dropdownContainer { + flex-direction: column; + } + + .dropdownItem { + min-width: 100%; + } + + .costPredictionTitle { + font-size: 16px; + } + + .costPredictionChartContainer { + height: 400px; + } + + .costPredictionPage { + padding: 10px; + } +} + +@media (max-width: 480px) { + .chartTitleContainer { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .costPredictionChartContainer { + height: 350px; + } +} \ No newline at end of file diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx index e5d032a325..3228995f94 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts'; -import axios from 'axios'; +import httpService from '../../../../services/httpService'; import styles from './ExpenditureChart.module.css'; const COLORS = ['#6777EF', '#A0CD61', '#F5CD4B']; @@ -46,7 +46,7 @@ function ExpenditureChart({ projectId }) { setLoading(true); setError(null); try { - const res = await axios.get( + const res = await httpService.get( `${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/${projectId}/pie`, ); setActual(res.data.actual); @@ -61,8 +61,8 @@ function ExpenditureChart({ projectId }) { }, [projectId]); const renderChart = (data, title) => ( -
-

{title}

+
+

{title}

Loading expenditure data...
; - if (error) return
{error}
; + return
Loading expenditure data...
; + if (error) return
{error}
; return ( -
+
{renderChart(normalizeData(actual), 'Actual Expenditure')} {renderChart(normalizeData(planned), 'Planned Expenditure')}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css index 8b58a523e4..2d2401b467 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css @@ -1,4 +1,4 @@ -.expenditure-chart-wrapper { +.expenditureChartWrapper { display: flex; justify-content: center; flex-wrap: wrap; @@ -8,7 +8,7 @@ margin: 0; } -.expenditure-chart-card { +.expenditureChartCard { flex: 1 1 220px; max-width: 260px; padding: 12px 10px; @@ -22,7 +22,7 @@ justify-content: flex-start; } -.expenditure-chart-title { +.expenditureChartTitle { font-size: 1.1rem; font-weight: bold; text-align: center; @@ -47,12 +47,12 @@ white-space: nowrap; } -.dark-mode .expenditure-chart-card { +.dark-mode .expenditureChartCard { background: var(--card-bg); box-shadow: 0 2px 5px rgba(255, 255, 255, 0.08); } -.dark-mode .expenditure-chart-title { +.dark-mode .expenditureChartTitle { color: var(--text-color); } @@ -61,3 +61,19 @@ color: #fff !important; border: 1px solid #555 !important; } + +.expenditureChartLoading { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + color: var(--text-color); +} + +.expenditureChartError { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + color: var(--error-color, #ff4d4f); +} diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx index d1eb5e79f5..953e1c91b8 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import axios from 'axios'; +import httpService from '../../../../services/httpService'; import ExpenditureChart from './ExpenditureChart'; function FinancialsTrackingCard() { @@ -12,7 +12,9 @@ function FinancialsTrackingCard() { const fetchProjects = async () => { try { setLoading(true); - const res = await axios.get(`${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/projects`); + const res = await httpService.get( + `${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/projects`, + ); const labeledProjects = res.data.map((id, index) => ({ id, name: `Project ${String.fromCharCode(65 + index)}`, @@ -22,7 +24,6 @@ function FinancialsTrackingCard() { setSelectedProject(labeledProjects[0].id); } } catch (err) { - // console.error('Error fetching project IDs:', err); setError('Failed to load projects'); } finally { setLoading(false); diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index ff1cf6ac5f..7f1deb5173 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -17,6 +17,7 @@ import ToolsHorizontalBarChart from './Tools/ToolsHorizontalBarChart'; import ExpenseBarChart from './Financials/ExpenseBarChart'; import ActualVsPlannedCost from './ActualVsPlannedCost/ActualVsPlannedCost'; import TotalMaterialCostPerProject from './TotalMaterialCostPerProject/TotalMaterialCostPerProject'; +import FinancialsTrackingCard from './ExpenditureChart/FinancialsTrackingCard'; import styles from './WeeklyProjectSummary.module.css'; import IssueCharts from '../Issues/openIssueCharts'; import MostFrequentKeywords from './MostFrequentKeywords/MostFrequentKeywords'; @@ -369,6 +370,7 @@ function WeeklyProjectSummary() { className={`${styles.weeklyProjectSummaryCard} ${styles.normalCard}`} > {(() => { + if (index === 0) return ; if (index === 2) return ; if (index === 3) return ; return 'πŸ“Š Card'; diff --git a/src/routes.jsx b/src/routes.jsx index 7aee4b2b03..1eebdf7272 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -116,6 +116,7 @@ import UtilizationChart from './components/BMDashboard/UtilizationChart/Utilizat import RentalChart from './components/BMDashboard/RentalChart/RentalChart'; import CreateNewTeam from './components/BMDashboard/Team/CreateNewTeam/CreateNewTeam'; +import CostPredictionPage from './components/BMDashboard/CostPrediction/CostPredictionPage'; // Community Portal import CPProtectedRoute from './components/common/CPDashboard/CPProtectedRoute'; @@ -600,6 +601,7 @@ export default ( component={PurchaseConsumable} /> + diff --git a/src/services/projectCostTrackingService.js b/src/services/projectCostTrackingService.js new file mode 100644 index 0000000000..8993827673 --- /dev/null +++ b/src/services/projectCostTrackingService.js @@ -0,0 +1,33 @@ +import axios from 'axios'; +import { ENDPOINTS } from '../utils/URL'; + +/** + * Fetches all available project IDs with cost tracking data + * @returns {Promise} Array of project IDs + */ +export const getProjectIds = async () => { + const response = await axios.get(ENDPOINTS.PROJECT_COST_IDS); + return response.data.projectIds || []; +}; + +/** + * Fetches cost data for a specific project + * @param {string} projectId - The ID of the project + * @param {Object} options - Optional parameters + * @param {string} options.categories - Comma-separated list of categories to filter by + * @param {string} options.fromDate - Start date for filtering (YYYY-MM-DD) + * @param {string} options.toDate - End date for filtering (YYYY-MM-DD) + * @returns {Promise} Cost data including actual and predicted costs + */ +export const getProjectCosts = async (projectId, options = {}) => { + const { categories, fromDate, toDate } = options; + const response = await axios.get( + ENDPOINTS.PROJECT_COSTS_BY_ID(projectId, categories, fromDate, toDate), + ); + return response.data; +}; + +export default { + getProjectIds, + getProjectCosts, +}; diff --git a/src/utils/URL.js b/src/utils/URL.js index 67be97b89c..7c07c79a50 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -269,6 +269,18 @@ export const ENDPOINTS = { BM_INJURY_PROJECTS: `${APIEndpoint}/bm/injuries/project-injury`, BM_INJURY_ISSUE: `${APIEndpoint}/bm/issues`, BM_RENTAL_CHART: `${APIEndpoint}/bm/rentalChart`, + + // Project cost tracking endpoints + PROJECT_COST_IDS: `${APIEndpoint}/bm/projects-cost/ids`, + PROJECT_COSTS_BY_ID: (projectId, categories, fromDate, toDate) => { + let url = `${APIEndpoint}/bm/projects/${projectId}/costs`; + const params = []; + if (categories) params.push(`categories=${categories}`); + if (fromDate) params.push(`fromDate=${fromDate}`); + if (toDate) params.push(`toDate=${toDate}`); + if (params.length > 0) url += `?${params.join('&')}`; + return url; + }, TOOLS_AVAILABILITY_PROJECTS: `${APIEndpoint}/bm/tools-availability/projects`, TOOLS_AVAILABILITY_BY_PROJECT: (projectId, startDate, endDate) => { let url = `${APIEndpoint}/bm/projects/${projectId}/tools-availability`; From 0165073c1e7b07239558fbf57e5cc96344dd594c Mon Sep 17 00:00:00 2001 From: shashankm9 Date: Sun, 8 Feb 2026 22:50:47 -0600 Subject: [PATCH 2/6] Add BM Dashboard Expenditure Pie Charts with mock data - Created FinancialsTrackingCard component with project dropdown - Created ExpenditureChart component with Actual/Planned pie charts - Added mock data from MongoDB expenditurePie collection - Implemented dark mode support and responsive design - Fixed legend overflow and dropdown visibility issues - Fixed Cost Prediction chart line connectivity with connectNulls prop --- .../CostPrediction/CostPredictionPage.jsx | 2 + .../ExpenditureChart/ExpenditureChart.jsx | 18 ++-- .../ExpenditureChart.module.css | 33 +++++- .../FinancialsTrackingCard.jsx | 23 ++-- .../FinancialsTrackingCard.module.css | 100 ++++++++++++++++++ .../ExpenditureChart/mockExpenditureData.js | 86 +++++++++++++++ 6 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.module.css create mode 100644 src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/mockExpenditureData.js diff --git a/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx b/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx index a9bf9df71f..7bcf6faa56 100644 --- a/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx +++ b/src/components/BMDashboard/CostPrediction/CostPredictionPage.jsx @@ -663,6 +663,7 @@ function CostPredictionPage({ projectId }) { name={`${category} Cost`} stroke={costColors[category]} strokeWidth={2} + connectNulls={true} dot={{ r: 3, fill: costColors[category], @@ -686,6 +687,7 @@ function CostPredictionPage({ projectId }) { stroke={costColors[category]} strokeWidth={2} strokeDasharray="8 4" + connectNulls={true} dot={getDotRenderer(category)} activeDot={{ r: 4 }} isAnimationActive={false} diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx index 3228995f94..85cf6a445a 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts'; -import httpService from '../../../../services/httpService'; +// import httpService from '../../../../services/httpService'; +import { getProjectExpenditure } from './mockExpenditureData'; import styles from './ExpenditureChart.module.css'; const COLORS = ['#6777EF', '#A0CD61', '#F5CD4B']; @@ -46,11 +47,16 @@ function ExpenditureChart({ projectId }) { setLoading(true); setError(null); try { - const res = await httpService.get( - `${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/${projectId}/pie`, - ); - setActual(res.data.actual); - setPlanned(res.data.planned); + // Using mock data instead of API call + const data = getProjectExpenditure(projectId); + setActual(data.actual); + setPlanned(data.planned); + // Original API call (commented out for mock data) + // const res = await httpService.get( + // `${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/${projectId}/pie`, + // ); + // setActual(res.data.actual); + // setPlanned(res.data.planned); } catch (err) { setError('Failed to load expenditure data'); } finally { diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css index 2d2401b467..e7c5519980 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css @@ -10,9 +10,9 @@ .expenditureChartCard { flex: 1 1 220px; - max-width: 260px; + max-width: 280px; padding: 12px 10px; - min-height: 300px; + min-height: 350px; background: var(--card-bg); box-shadow: 0 2px 4px var(--card-shadow); border-radius: 8px; @@ -27,15 +27,18 @@ font-weight: bold; text-align: center; margin-bottom: 6px; + color: var(--text-color); } .recharts-legend-wrapper { margin-top: -10px !important; + padding: 0 10px; } .recharts-legend-item-text { - font-size: 12px !important; + font-size: 11px !important; white-space: nowrap; + color: var(--text-color) !important; } .recharts-pie-label-text { @@ -77,3 +80,27 @@ padding: 20px; color: var(--error-color, #ff4d4f); } + +/* Responsive design for tablets and smaller screens */ +@media (max-width: 768px) { + .expenditureChartCard { + max-width: 100%; + flex: 1 1 100%; + } + + .expenditureChartWrapper { + gap: 12px; + } +} + +/* Responsive design for mobile */ +@media (max-width: 480px) { + .expenditureChartCard { + padding: 8px; + min-height: 260px; + } + + .expenditureChartTitle { + font-size: 1rem; + } +} diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx index 953e1c91b8..ddd6ae932a 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react'; -import httpService from '../../../../services/httpService'; +// import httpService from '../../../../services/httpService'; import ExpenditureChart from './ExpenditureChart'; +import { getProjectIds } from './mockExpenditureData'; +import styles from './FinancialsTrackingCard.module.css'; function FinancialsTrackingCard() { const [projectList, setProjectList] = useState([]); @@ -12,13 +14,20 @@ function FinancialsTrackingCard() { const fetchProjects = async () => { try { setLoading(true); - const res = await httpService.get( - `${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/projects`, - ); - const labeledProjects = res.data.map((id, index) => ({ + // Using mock data instead of API call + const projectIds = getProjectIds(); + const labeledProjects = projectIds.map((id, index) => ({ id, name: `Project ${String.fromCharCode(65 + index)}`, })); + // Original API call (commented out for mock data) + // const res = await httpService.get( + // `${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/projects`, + // ); + // const labeledProjects = res.data.map((id, index) => ({ + // id, + // name: `Project ${String.fromCharCode(65 + index)}`, + // })); setProjectList(labeledProjects); if (labeledProjects.length > 0) { setSelectedProject(labeledProjects[0].id); @@ -36,8 +45,8 @@ function FinancialsTrackingCard() { if (error) return
{error}
; return ( -
-
+
+
({ - ...baseStyles, - backgroundColor: '#2c3344', - borderColor: '#364156', - }), - menu: baseStyles => ({ - ...baseStyles, - backgroundColor: '#2c3344', - }), - option: (baseStyles, state) => ({ - ...baseStyles, - backgroundColor: state.isFocused ? '#364156' : '#2c3344', - color: '#e0e0e0', - }), - multiValue: baseStyles => ({ - ...baseStyles, - backgroundColor: '#364156', - }), - multiValueLabel: baseStyles => ({ - ...baseStyles, - color: '#e0e0e0', - }), - multiValueRemove: baseStyles => ({ - ...baseStyles, - color: '#e0e0e0', - ':hover': { backgroundColor: '#ff4d4f', color: '#fff' }, - }), - } - : {} - } + styles={getMultiSelectStyles(darkMode)} />
@@ -573,21 +486,15 @@ function CostPredictionPage({ projectId }) {
- + `${currency}${value}`} width={50} /> @@ -603,11 +510,11 @@ function CostPredictionPage({ projectId }) { width: '100%', textAlign: 'center', lineHeight: '1.2', - color: darkMode ? '#e0e0e0' : '#333', + color: getAxisTickFill(darkMode), }} align="center" content={props => { - const { payload } = props; + const legendTextColor = getAxisTickFill(darkMode); return (
- {payload.map((entry, index) => { + {props.payload.map((entry, index) => { const isPredicted = entry.value.includes('Predicted'); return (
- {isPredicted ? ( - // Solid diamond shape for predicted -
- ) : ( - // Solid circle for actual -
- )} - - {entry.value} - + + {entry.value}
); })} @@ -729,7 +614,7 @@ function CostPredictionPage({ projectId }) { {!loading && !error && data.length === 0 && (

No data available

diff --git a/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css b/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css index 813dd919dd..b9caaede0f 100644 --- a/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css +++ b/src/components/BMDashboard/CostPrediction/CostPredictionPage.module.css @@ -184,6 +184,33 @@ color: #ffffff; } +/* Dark-mode body overrides for Recharts (replaces injected