From 155923a8e86edb3b434e6d1065999c4e92a4407c Mon Sep 17 00:00:00 2001 From: Manoj Puttaswamy Date: Fri, 3 Apr 2026 01:11:10 -0500 Subject: [PATCH 1/9] "added tool count to the tool tip" --- .../BMDashboard/UtilizationChart/UtilizationChart.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx index 6b7332fe96..6ad3938090 100644 --- a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx +++ b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx @@ -108,7 +108,7 @@ function UtilizationChart() { callbacks: { label: context => { const tool = toolsData[context.dataIndex]; - return `Utilization: ${tool.utilizationRate}%, Downtime: ${tool.downtime} hrs`; + return `Utilization: ${tool.utilizationRate}%, Downtime: ${tool.downtime} hrs, Tool count: ${tool.count}`; }, }, footerColor: 'white', From f3000ca5c2402b67cfa83217d8c28db73eeda7e6 Mon Sep 17 00:00:00 2001 From: Manoj Puttaswamy Date: Fri, 3 Apr 2026 01:59:52 -0500 Subject: [PATCH 2/9] "feat: added compare toggle button and comparision with prev data " --- .../UtilizationChart/UtilizationChart.jsx | 121 ++++++++++++++++-- .../UtilizationChart.module.css | 43 ++++++- 2 files changed, 155 insertions(+), 9 deletions(-) diff --git a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx index 6ad3938090..8c36baea07 100644 --- a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx +++ b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx @@ -1,6 +1,6 @@ /* eslint-disable */ /* prettier-ignore */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Title } from 'chart.js'; import { Bar } from 'react-chartjs-2'; import DatePicker from 'react-datepicker'; @@ -15,6 +15,7 @@ ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Title, ChartDa function UtilizationChart() { const [toolsData, setToolsData] = useState([]); + const [previousToolsData, setPreviousToolsData] = useState([]); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [toolFilter, setToolFilter] = useState('ALL'); @@ -22,9 +23,30 @@ function UtilizationChart() { const [error, setError] = useState(null); const [toolTypes, setToolTypes] = useState([]); const [projects, setProjects] = useState([]); + const [comparisonMode, setComparisonMode] = useState(false); const darkMode = useSelector(state => state.theme.darkMode); - const fetchChartData = async () => { + //mock data to test compare functionality. + const getMockPreviousData = data => { + return data.map(tool => ({ + ...tool, + utilizationRate: Math.max(0, tool.utilizationRate + Math.floor(Math.random() * 21) - 15), // ±10% random + downtime: Math.round(tool.downtime * (0.85 + Math.random() * 0.3)), // ±15% random + })); + }; + + // Compute previous period date range based on current selection + const getPreviousPeriod = () => { + const end = endDate ? new Date(endDate) : new Date(); + const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const duration = end - start; + return { + prevStart: new Date(start - duration), + prevEnd: new Date(start), + }; + }; + + const fetchChartData = async (currentComparisonMode = comparisonMode) => { try { const response = await axios.get(`${process.env.REACT_APP_APIENDPOINT}/tools/utilization`, { params: { @@ -38,6 +60,28 @@ function UtilizationChart() { }, }); setToolsData(response.data); + + if (currentComparisonMode) { + const { prevStart, prevEnd } = getPreviousPeriod(); + const prevResponse = await axios.get( + `${process.env.REACT_APP_APIENDPOINT}/tools/utilization`, + { + params: { + startDate: prevStart, + endDate: prevEnd, + tool: toolFilter, + project: projectFilter, + }, + headers: { + Authorization: localStorage.getItem('token'), + }, + }, + ); + //setPreviousToolsData(prevResponse.data); //uncomment for real response from BE + setPreviousToolsData(getMockPreviousData(response.data)); + } else { + setPreviousToolsData([]); + } } catch (err) { setError('Failed to load utilization data.'); } @@ -73,6 +117,13 @@ function UtilizationChart() { fetchChartData(); }; + // Auto-fetch when comparison mode is toggled + const handleComparisonToggle = () => { + const newMode = !comparisonMode; + setComparisonMode(newMode); + fetchChartData(newMode); + }; + const chartData = { labels: toolsData.map(tool => tool.name), datasets: [ @@ -93,22 +144,58 @@ function UtilizationChart() { labels: { color: darkMode ? '#ffffff' : '#333' }, }, datalabels: { - color: darkMode ? '#ffffff' : '#333', + color: context => { + if (!comparisonMode || !previousToolsData.length) { + return darkMode ? '#ffffff' : '#333'; + } + const tool = toolsData[context.dataIndex]; + const prev = previousToolsData.find(p => p.name === tool.name); + if (!prev) return darkMode ? '#ffffff' : '#333'; + return tool.utilizationRate >= prev.utilizationRate ? '#4caf50' : '#f44336'; + }, anchor: 'end', align: 'end', - font: { - size: 12, - }, + font: { size: 12 }, formatter: (_, context) => { const tool = toolsData[context.dataIndex]; - return `${tool.downtime} hrs`; + const baseLabel = `${tool.downtime} hrs`; + + if (!comparisonMode || !previousToolsData.length) return baseLabel; + + const prev = previousToolsData.find(p => p.name === tool.name); + if (!prev) return baseLabel; + + const delta = tool.utilizationRate - prev.utilizationRate; + const arrow = delta >= 0 ? '↑' : '↓'; + return `${baseLabel} ${arrow} ${Math.abs(delta)}%`; }, }, tooltip: { callbacks: { + title: context => { + const tool = toolsData[context[0].dataIndex]; + return tool.name; + }, label: context => { const tool = toolsData[context.dataIndex]; - return `Utilization: ${tool.utilizationRate}%, Downtime: ${tool.downtime} hrs, Tool count: ${tool.count}`; + const lines = [ + `Utilization: ${tool.utilizationRate}%`, + `Downtime: ${tool.downtime} hrs`, + `Count: ${tool.count ?? 'N/A'} tool(s)`, + ]; + + if (comparisonMode && previousToolsData.length) { + const prev = previousToolsData.find(p => p.name === tool.name); + if (prev) { + const delta = tool.utilizationRate - prev.utilizationRate; + const arrow = delta >= 0 ? '↑' : '↓'; + lines.push( + `vs Previous: ${arrow} ${Math.abs(delta)}% (was ${prev.utilizationRate}%)`, + ); + } + } + + return lines; }, }, footerColor: 'white', @@ -190,8 +277,26 @@ function UtilizationChart() { + + + {comparisonMode && ( +

+ Showing comparison with previous {startDate && endDate ? 'selected' : '30-day'}{' '} + period. + ↑ Improved + ↓ Declined +

+ )} + )} diff --git a/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css b/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css index 43ca101cb8..f56a105dc7 100644 --- a/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css +++ b/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css @@ -102,6 +102,43 @@ background-color: var(--button-hover, #333); } +/* Compare toggle - ON state */ +.activeButton { + background-color: #4caf50 !important; + color: #fff !important; + border: none; +} + +.activeButton:hover { + background-color: #43a047 !important; +} + +/* Compare toggle - OFF state */ +.inactiveButton { + background-color: var(--button-bg, #000); + color: var(--button-text, #fff); +} + +/* Comparison mode hint note */ +.comparisonNote { + text-align: center; + font-size: 0.85rem; + margin-bottom: 1rem; + color: var(--text-color, #555); +} + +.legendUp { + color: #4caf50; + font-weight: 600; + margin-left: 0.5rem; +} + +.legendDown { + color: #f44336; + font-weight: 600; + margin-left: 0.5rem; +} + .utilizationChartError { color: var(--error-color, red); text-align: center; @@ -126,6 +163,10 @@ color: #fff; } +.utilizationChartContainer.darkMode .comparisonNote { + color: #ccc; +} + /* React-DatePicker Dark Mode Dropdown Styling */ .utilizationChartContainer.darkMode :global(.react-datepicker) { background-color: #333; @@ -153,4 +194,4 @@ .utilizationChartContainer.darkMode :global(.react-datepicker__day--in-range) { background-color: #e8a71c; color: #000; -} +} \ No newline at end of file From 966647647e2217be3249bd3fbd9f033a7893144d Mon Sep 17 00:00:00 2001 From: Manoj Puttaswamy Date: Sat, 4 Apr 2026 00:33:47 -0500 Subject: [PATCH 3/9] "feat: summary and key insights of tools utilization" --- .../UtilizationChart/UtilizationChart.jsx | 90 +++++++++++++++---- .../UtilizationChart.module.css | 75 +++++++++++++++- 2 files changed, 146 insertions(+), 19 deletions(-) diff --git a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx index 8c36baea07..4adf9ff1c4 100644 --- a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx +++ b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx @@ -1,6 +1,6 @@ /* eslint-disable */ /* prettier-ignore */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Title } from 'chart.js'; import { Bar } from 'react-chartjs-2'; import DatePicker from 'react-datepicker'; @@ -26,12 +26,12 @@ function UtilizationChart() { const [comparisonMode, setComparisonMode] = useState(false); const darkMode = useSelector(state => state.theme.darkMode); - //mock data to test compare functionality. + // Mock data to test compare functionality const getMockPreviousData = data => { return data.map(tool => ({ ...tool, - utilizationRate: Math.max(0, tool.utilizationRate + Math.floor(Math.random() * 21) - 15), // ±10% random - downtime: Math.round(tool.downtime * (0.85 + Math.random() * 0.3)), // ±15% random + utilizationRate: Math.max(0, tool.utilizationRate + Math.floor(Math.random() * 21) - 15), + downtime: Math.round(tool.downtime * (0.85 + Math.random() * 0.3)), })); }; @@ -77,7 +77,7 @@ function UtilizationChart() { }, }, ); - //setPreviousToolsData(prevResponse.data); //uncomment for real response from BE + // setPreviousToolsData(prevResponse.data); setPreviousToolsData(getMockPreviousData(response.data)); } else { setPreviousToolsData([]); @@ -91,14 +91,10 @@ function UtilizationChart() { try { const [toolTypesResponse, projectsResponse] = await Promise.all([ axios.get(ENDPOINTS.BM_TOOL_TYPES, { - headers: { - Authorization: localStorage.getItem('token'), - }, + headers: { Authorization: localStorage.getItem('token') }, }), axios.get(ENDPOINTS.BM_PROJECTS + 'Names', { - headers: { - Authorization: localStorage.getItem('token'), - }, + headers: { Authorization: localStorage.getItem('token') }, }), ]); setToolTypes(toolTypesResponse.data); @@ -124,6 +120,28 @@ function UtilizationChart() { fetchChartData(newMode); }; + // Summary banner computed values + const summary = useMemo(() => { + if (!toolsData.length) return null; + + const totalTools = toolsData.reduce((sum, t) => sum + (t.count || 0), 0); + const avgUtilization = + Math.round( + (toolsData.reduce((sum, t) => sum + t.utilizationRate, 0) / toolsData.length) * 10, + ) / 10; + const mostUtilized = toolsData[0]; // already sorted desc by backend + const totalDowntime = toolsData.reduce((sum, t) => sum + t.downtime, 0); + + let avgUtilizationChange = null; + if (comparisonMode && previousToolsData.length) { + const prevAvg = + previousToolsData.reduce((sum, t) => sum + t.utilizationRate, 0) / previousToolsData.length; + avgUtilizationChange = Math.round((avgUtilization - prevAvg) * 10) / 10; + } + + return { totalTools, avgUtilization, mostUtilized, totalDowntime, avgUtilizationChange }; + }, [toolsData, previousToolsData, comparisonMode]); + const chartData = { labels: toolsData.map(tool => tool.name), datasets: [ @@ -159,12 +177,9 @@ function UtilizationChart() { formatter: (_, context) => { const tool = toolsData[context.dataIndex]; const baseLabel = `${tool.downtime} hrs`; - if (!comparisonMode || !previousToolsData.length) return baseLabel; - const prev = previousToolsData.find(p => p.name === tool.name); if (!prev) return baseLabel; - const delta = tool.utilizationRate - prev.utilizationRate; const arrow = delta >= 0 ? '↑' : '↓'; return `${baseLabel} ${arrow} ${Math.abs(delta)}%`; @@ -183,7 +198,6 @@ function UtilizationChart() { `Downtime: ${tool.downtime} hrs`, `Count: ${tool.count ?? 'N/A'} tool(s)`, ]; - if (comparisonMode && previousToolsData.length) { const prev = previousToolsData.find(p => p.name === tool.name); if (prev) { @@ -194,7 +208,6 @@ function UtilizationChart() { ); } } - return lines; }, }, @@ -230,6 +243,51 @@ function UtilizationChart() {
{error}
) : ( <> + {/* Summary Banner */} + {summary && ( +
+
+ Total Tools + {summary.totalTools} +
+ +
+ Avg Utilization + {summary.avgUtilization}% + {summary.avgUtilizationChange !== null && ( + = 0 + ? styles.summaryChangeUp + : styles.summaryChangeDown + } + > + {summary.avgUtilizationChange >= 0 ? '↑' : '↓'}{' '} + {Math.abs(summary.avgUtilizationChange)}% vs prev + + )} +
+ +
+ Top Tool + + {summary.mostUtilized?.name ?? 'N/A'} + + + {summary.mostUtilized?.utilizationRate ?? 0}% utilized + +
+ +
+ Total Downtime + + {summary.totalDowntime.toLocaleString()} hrs + +
+
+ )} + + {/* Filters */}