From e38c74f7c1263c9a00e6dc752b51af9aeb290a90 Mon Sep 17 00:00:00 2001 From: Isha Date: Fri, 23 Oct 2026 21:18:27 -0700 Subject: [PATCH 1/3] feat: add rental cost breakdown chart --- .../BMDashboard/RentalChart/RentalChart.jsx | 199 ++++++++++++++++-- src/utils/URL.js | 2 + 2 files changed, 182 insertions(+), 19 deletions(-) diff --git a/src/components/BMDashboard/RentalChart/RentalChart.jsx b/src/components/BMDashboard/RentalChart/RentalChart.jsx index 83eeefe5a87..3facbfddb8f 100644 --- a/src/components/BMDashboard/RentalChart/RentalChart.jsx +++ b/src/components/BMDashboard/RentalChart/RentalChart.jsx @@ -2,7 +2,7 @@ import { useRef, useEffect, useState, useMemo } from 'react'; import { useSelector } from 'react-redux'; import axios from 'axios'; import { ENDPOINTS } from '~/utils/URL'; -import { Line } from 'react-chartjs-2'; +import { Line, Bar } from 'react-chartjs-2'; import DatePicker from 'react-datepicker'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import styles from './RentalChart.module.css'; @@ -14,12 +14,22 @@ import { LinearScale, PointElement, LineElement, + BarElement, Title, Tooltip, Legend, } from 'chart.js'; -ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + Legend, +); const PROJECT_COLORS = [ { borderColor: 'rgb(53, 162, 235)', backgroundColor: 'rgba(53, 162, 235, 0.5)' }, @@ -212,6 +222,8 @@ export default function RentalChart() { const [fallbackProjectNames, setFallbackProjectNames] = useState(new Map()); const [rawData, setRawData] = useState([]); + const [rentalCostData, setRentalCostData] = useState([]); + const [costBreakdownData, setCostBreakdownData] = useState([]); // Only list projects that have rental data; prefer names from the BM projects list const availableProjects = useMemo(() => { @@ -247,19 +259,100 @@ export default function RentalChart() { } }, [availableTools, selectedTool]); - const chartData = useMemo(() => { - const filteredData = filterRentalData(rawData, selectedProject, selectedTool, dateRange); - const { labels, monthsInRange, totalMonths } = buildMonthsRange(dateRange); - const groupMap = aggregateDataByProject( - filteredData, - monthsInRange, - totalMonths, - chartType, - availableProjects, - ); - return { labels, datasets: buildDatasetsFromGroupMap(groupMap) }; - }, [rawData, selectedProject, selectedTool, dateRange, chartType, availableProjects]); + const chartData = useMemo( + () => ({ + labels: rentalCostData.map(item => item.date), + datasets: [ + { + label: 'Total Rental Cost', + data: rentalCostData.map(item => item.value), + borderColor: 'rgb(53, 162, 235)', + backgroundColor: 'rgba(53, 162, 235, 0.5)', + tension: 0.4, + fill: false, + }, + ], + }), + [rentalCostData], + ); + const stackedBarData = useMemo( + () => ({ + labels: costBreakdownData.map(item => item.projectName), + datasets: [ + { + label: 'Tools costs due to owned tools', + data: costBreakdownData.map(item => item.ownedToolsCost), + backgroundColor: 'rgba(53, 162, 235, 0.7)', + }, + { + label: 'Tools costs due to rented tools', + data: costBreakdownData.map(item => item.rentedToolsCost), + backgroundColor: 'rgba(255, 99, 132, 0.7)', + }, + ], + }), + [costBreakdownData], + ); + + const stackedBarOptions = useMemo(() => { + const textColor = darkMode ? '#ffffff' : '#000000'; + const gridColor = darkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; + + return { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + labels: { + color: textColor, + }, + }, + title: { + display: true, + text: 'How Much of Tools Costs Are Due to Rentals?', + color: textColor, + }, + datalabels: { + display: true, + color: textColor, + formatter: value => `$${Number(value).toFixed(2)}`, + }, + }, + scales: { + x: { + stacked: true, + title: { + display: true, + text: 'Projects', + color: textColor, + }, + ticks: { + color: textColor, + }, + grid: { + color: gridColor, + }, + }, + y: { + stacked: true, + beginAtZero: true, + title: { + display: true, + text: 'Cost', + color: textColor, + }, + ticks: { + color: textColor, + }, + grid: { + color: gridColor, + }, + }, + }, + }; + }, [darkMode]); useEffect(() => { const fetchRentalData = async () => { try { @@ -314,6 +407,65 @@ export default function RentalChart() { // Errors are handled inside fetchRentalData fetchRentalData().catch(() => {}); }, []); + useEffect(() => { + const fetchChartData = async () => { + try { + const headers = { + Authorization: localStorage.getItem('token'), + }; + + const params = { + startDate: dateRange.startDate.toISOString(), + endDate: dateRange.endDate.toISOString(), + }; + + if (selectedProject !== FILTER_ALL) { + params.projectId = selectedProject; + } + + const breakdownParams = { + startDate: dateRange.startDate.toISOString(), + endDate: dateRange.endDate.toISOString(), + }; + + if (selectedProject !== FILTER_ALL) { + breakdownParams.projects = selectedProject; + } + + const [rentalResponse, breakdownResponse, projectsResponse] = await Promise.all([ + axios.get(ENDPOINTS.BM_RENTAL_COST_OVER_TIME, { + headers, + params, + }), + axios.get(ENDPOINTS.BM_TOOLS_COST_BREAKDOWN, { + headers, + params: breakdownParams, + }), + axios.get(ENDPOINTS.BM_TOOLS_RETURNED_LATE_PROJECTS, { + headers, + }), + ]); + + const projects = projectsResponse.data?.data || []; + + const projectNames = new Map( + projects.map(project => [project.projectId, project.projectName]), + ); + + const breakdownWithNames = breakdownResponse.data.map(item => ({ + ...item, + projectName: projectNames.get(item.projectId) || item.projectName || 'Unknown Project', + })); + + setRentalCostData(rentalResponse.data); + setCostBreakdownData(breakdownWithNames); + } catch (err) { + toast.error('Error fetching rental cost chart data'); + } + }; + + fetchChartData(); + }, [selectedProject, dateRange]); // Track the chart area's width so fonts and labels can shrink on small screens useEffect(() => { @@ -483,11 +635,11 @@ export default function RentalChart() { ); } - if (error) { - return ( -
{error}
- ); - } + // if (error) { + // return ( + //
{error}
+ // ); + // } if (chartData.datasets.length === 0) { return ( @@ -622,6 +774,15 @@ export default function RentalChart() { > {renderChartContent()} +
+ {costBreakdownData.length > 0 ? ( + + ) : ( +
+ No tool cost breakdown data available for the selected filters +
+ )} +
); } diff --git a/src/utils/URL.js b/src/utils/URL.js index 9a183aef8c6..e8433e100c7 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -445,6 +445,8 @@ export const ENDPOINTS = { BM_INJURY_ISSUE: `${APIEndpoint}/bm/issues`, BM_INJURY_SEVERITY: `${APIEndpoint}/bm/injuries/severity-by-project`, BM_RENTAL_CHART: `${APIEndpoint}/bm/rentalChart`, + BM_RENTAL_COST_OVER_TIME: `${APIEndpoint}/bm/rentals/cost-over-time`, + BM_TOOLS_COST_BREAKDOWN: `${APIEndpoint}/bm/tools/cost-breakdown`, BM_TOOLS_RETURNED_LATE: `${APIEndpoint}/bm/tools/returned-late`, BM_TOOLS_RETURNED_LATE_PROJECTS: `${APIEndpoint}/bm/tools/returned-late/projects`, From e47188176ba60acd8f8b5de8722981915f8d072c Mon Sep 17 00:00:00 2001 From: Isha Date: Fri, 23 Oct 2026 22:45:28 -0700 Subject: [PATCH 2/3] fix: address rental chart code quality warnings --- .../BMDashboard/RentalChart/RentalChart.jsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/BMDashboard/RentalChart/RentalChart.jsx b/src/components/BMDashboard/RentalChart/RentalChart.jsx index 3facbfddb8f..f42d16550e6 100644 --- a/src/components/BMDashboard/RentalChart/RentalChart.jsx +++ b/src/components/BMDashboard/RentalChart/RentalChart.jsx @@ -459,12 +459,13 @@ export default function RentalChart() { setRentalCostData(rentalResponse.data); setCostBreakdownData(breakdownWithNames); - } catch (err) { + } catch { toast.error('Error fetching rental cost chart data'); + setError('Error loading rental cost chart data'); } }; - fetchChartData(); + void fetchChartData(); }, [selectedProject, dateRange]); // Track the chart area's width so fonts and labels can shrink on small screens @@ -635,11 +636,11 @@ export default function RentalChart() { ); } - // if (error) { - // return ( - //
{error}
- // ); - // } + if (error) { + return ( +
{error}
+ ); + } if (chartData.datasets.length === 0) { return ( From ff6b91204add5ac31ca0dde2f997506874d4d4b8 Mon Sep 17 00:00:00 2001 From: Isha Date: Wed, 4 Nov 2026 01:35:26 -0500 Subject: [PATCH 3/3] fix: handle empty rental chart data gracefully --- .../BMDashboard/RentalChart/RentalChart.jsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/BMDashboard/RentalChart/RentalChart.jsx b/src/components/BMDashboard/RentalChart/RentalChart.jsx index f42d16550e6..211914231f5 100644 --- a/src/components/BMDashboard/RentalChart/RentalChart.jsx +++ b/src/components/BMDashboard/RentalChart/RentalChart.jsx @@ -432,7 +432,7 @@ export default function RentalChart() { breakdownParams.projects = selectedProject; } - const [rentalResponse, breakdownResponse, projectsResponse] = await Promise.all([ + const [rentalResult, breakdownResult, projectsResult] = await Promise.allSettled([ axios.get(ENDPOINTS.BM_RENTAL_COST_OVER_TIME, { headers, params, @@ -445,20 +445,39 @@ export default function RentalChart() { headers, }), ]); + if (rentalResult.status === 'rejected') { + toast.error('Error fetching rental cost chart data'); + setError('Error loading rental cost chart data'); + return; + } + + const rentalData = + rentalResult.status === 'fulfilled' && Array.isArray(rentalResult.value.data) + ? rentalResult.value.data + : []; + + const breakdownData = + breakdownResult.status === 'fulfilled' && Array.isArray(breakdownResult.value.data) + ? breakdownResult.value.data + : []; - const projects = projectsResponse.data?.data || []; + const projects = + projectsResult.status === 'fulfilled' && Array.isArray(projectsResult.value.data?.data) + ? projectsResult.value.data.data + : []; const projectNames = new Map( projects.map(project => [project.projectId, project.projectName]), ); - const breakdownWithNames = breakdownResponse.data.map(item => ({ + const breakdownWithNames = breakdownData.map(item => ({ ...item, projectName: projectNames.get(item.projectId) || item.projectName || 'Unknown Project', })); - setRentalCostData(rentalResponse.data); + setRentalCostData(rentalData); setCostBreakdownData(breakdownWithNames); + setError(null); } catch { toast.error('Error fetching rental cost chart data'); setError('Error loading rental cost chart data'); @@ -642,7 +661,7 @@ export default function RentalChart() { ); } - if (chartData.datasets.length === 0) { + if (rentalCostData.length === 0) { return (
No data available for the selected filters