diff --git a/src/components/BMDashboard/UtilizationChart/ExportReportButton.jsx b/src/components/BMDashboard/UtilizationChart/ExportReportButton.jsx
new file mode 100644
index 0000000000..8d767e528f
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/ExportReportButton.jsx
@@ -0,0 +1,71 @@
+import { useState } from 'react';
+import axios from 'axios';
+import { toast } from 'react-toastify';
+import { ENDPOINTS } from '../../../utils/URL';
+import { EXPORT_FORMATS } from './constants';
+import styles from './UtilizationChart.module.css';
+
+function ExportReportButton({ tool, project, startDate, endDate }) {
+ const [exportingFormat, setExportingFormat] = useState(null);
+
+ const handleExport = async format => {
+ setExportingFormat(format);
+ try {
+ const params = {
+ format,
+ tool,
+ project,
+ ...(startDate && { startDate: startDate.toISOString() }),
+ ...(endDate && { endDate: endDate.toISOString() }),
+ };
+ const response = await axios.get(ENDPOINTS.BM_TOOL_UTILIZATION_EXPORT, {
+ params,
+ headers: { Authorization: localStorage.getItem('token') },
+ responseType: 'blob',
+ });
+
+ const contentDisposition = response.headers['content-disposition'] || '';
+ const filenameMatch = contentDisposition.match(/filename="?([^"]+)"?/);
+ const filename = filenameMatch ? filenameMatch[1] : `tool-utilization-report.${format}`;
+
+ const url = window.URL.createObjectURL(response.data);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+ } catch {
+ toast.error(`Failed to export ${format.toUpperCase()} report.`);
+ } finally {
+ setExportingFormat(null);
+ }
+ };
+
+ return (
+
+ Export Report:
+
+
+
+ );
+}
+
+export default ExportReportButton;
diff --git a/src/components/BMDashboard/UtilizationChart/ForecastModeToggle.jsx b/src/components/BMDashboard/UtilizationChart/ForecastModeToggle.jsx
new file mode 100644
index 0000000000..62fcf8f6ab
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/ForecastModeToggle.jsx
@@ -0,0 +1,51 @@
+import { useRef } from 'react';
+import { FORECAST_MODE_LABELS } from './constants';
+import styles from './UtilizationChart.module.css';
+
+const MODES = Object.entries(FORECAST_MODE_LABELS);
+
+function ForecastModeToggle({ value, onChange }) {
+ const buttonRefs = useRef([]);
+
+ const handleKeyDown = (e, index) => {
+ const count = MODES.length;
+ let nextIndex = -1;
+
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
+ e.preventDefault();
+ nextIndex = (index + 1) % count;
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
+ e.preventDefault();
+ nextIndex = (index - 1 + count) % count;
+ }
+
+ if (nextIndex !== -1) {
+ onChange(MODES[nextIndex][0]);
+ buttonRefs.current[nextIndex]?.focus();
+ }
+ };
+
+ return (
+
+ {MODES.map(([mode, label], index) => (
+
+ ))}
+
+ );
+}
+
+export default ForecastModeToggle;
diff --git a/src/components/BMDashboard/UtilizationChart/InsightsSummaryBar.jsx b/src/components/BMDashboard/UtilizationChart/InsightsSummaryBar.jsx
new file mode 100644
index 0000000000..3703ee850d
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/InsightsSummaryBar.jsx
@@ -0,0 +1,32 @@
+import styles from './UtilizationChart.module.css';
+
+function InsightsSummaryBar({ summary }) {
+ if (!summary) return null;
+
+ return (
+
+
+ {summary.totalToolTypes}
+ Tool Types
+
+
+ {summary.averageUtilization}%
+ Avg Utilization
+
+
+ {summary.normal}
+ Normal
+
+
+ {summary.underUtilized}
+ Under-utilized
+
+
+ {summary.overUtilized}
+ Over-utilized
+
+
+ );
+}
+
+export default InsightsSummaryBar;
diff --git a/src/components/BMDashboard/UtilizationChart/MaintenanceAlertPanel.jsx b/src/components/BMDashboard/UtilizationChart/MaintenanceAlertPanel.jsx
new file mode 100644
index 0000000000..53f3a924f3
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/MaintenanceAlertPanel.jsx
@@ -0,0 +1,42 @@
+import { URGENCY_STYLES } from './constants';
+import styles from './UtilizationChart.module.css';
+
+function MaintenanceAlertPanel({ alerts }) {
+ const sortedAlerts = [...alerts].sort((a, b) =>
+ a.urgency === 'high' && b.urgency !== 'high' ? -1 : 1,
+ );
+
+ return (
+
+
+ Maintenance Alerts
+
+ {sortedAlerts.length === 0 ? (
+ No maintenance alerts.
+ ) : (
+
+ )}
+
+ );
+}
+
+export default MaintenanceAlertPanel;
diff --git a/src/components/BMDashboard/UtilizationChart/RecommendationPanel.jsx b/src/components/BMDashboard/UtilizationChart/RecommendationPanel.jsx
new file mode 100644
index 0000000000..64d2bd3f73
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/RecommendationPanel.jsx
@@ -0,0 +1,37 @@
+import { TRAFFIC_LIGHT_COLORS } from './constants';
+import styles from './UtilizationChart.module.css';
+
+function RecommendationPanel({ recommendations }) {
+ return (
+
+
+ Utilization Recommendations
+
+ {recommendations.length === 0 ? (
+ No recommendations available.
+ ) : (
+
+ {recommendations.map(rec => (
+ -
+
+
+
{rec.toolName}
+
+ {rec.label}
+
+
{rec.action}
+
{rec.utilizationRate}% utilization
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default RecommendationPanel;
diff --git a/src/components/BMDashboard/UtilizationChart/ResourceBalancingPanel.jsx b/src/components/BMDashboard/UtilizationChart/ResourceBalancingPanel.jsx
new file mode 100644
index 0000000000..8183d78e84
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/ResourceBalancingPanel.jsx
@@ -0,0 +1,35 @@
+import styles from './UtilizationChart.module.css';
+
+function ResourceBalancingPanel({ suggestions }) {
+ return (
+
+
+ Resource Balancing
+
+ {suggestions.length === 0 ? (
+ Resources are balanced. No action needed.
+ ) : (
+
+ )}
+
+ );
+}
+
+export default ResourceBalancingPanel;
diff --git a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx
index 6b7332fe96..faa8ede5b2 100644
--- a/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx
+++ b/src/components/BMDashboard/UtilizationChart/UtilizationChart.jsx
@@ -1,200 +1,356 @@
-/* eslint-disable */
-/* prettier-ignore */
-import { useState, useEffect } from 'react';
-import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Title } from 'chart.js';
+import { useState, useEffect, useMemo, useCallback } from 'react';
+import {
+ Chart as ChartJS,
+ BarElement,
+ CategoryScale,
+ LinearScale,
+ Tooltip,
+ Title,
+ Legend,
+} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import axios from 'axios';
-import styles from './UtilizationChart.module.css';
import { useSelector } from 'react-redux';
-import { ENDPOINTS } from '../../../utils/URL';
+import { toast } from 'react-toastify';
import ChartDataLabels from 'chartjs-plugin-datalabels';
+import { ENDPOINTS } from '../../../utils/URL';
+import { FORECAST_MODES, TRAFFIC_LIGHT_COLORS } from './constants';
+import { useUtilizationData } from './hooks/useUtilizationData';
+import { useUtilizationInsights } from './hooks/useUtilizationInsights';
+import ForecastModeToggle from './ForecastModeToggle';
+import InsightsSummaryBar from './InsightsSummaryBar';
+import RecommendationPanel from './RecommendationPanel';
+import MaintenanceAlertPanel from './MaintenanceAlertPanel';
+import ResourceBalancingPanel from './ResourceBalancingPanel';
+import ExportReportButton from './ExportReportButton';
+import styles from './UtilizationChart.module.css';
-ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Title, ChartDataLabels);
+ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Title, Legend, ChartDataLabels);
+
+const getBarColor = trafficLight => TRAFFIC_LIGHT_COLORS[trafficLight] || '#94a3b8';
function UtilizationChart() {
- const [toolsData, setToolsData] = useState([]);
+ const [forecastMode, setForecastMode] = useState(FORECAST_MODES.HISTORICAL);
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
const [toolFilter, setToolFilter] = useState('ALL');
const [projectFilter, setProjectFilter] = useState('ALL');
- const [error, setError] = useState(null);
const [toolTypes, setToolTypes] = useState([]);
const [projects, setProjects] = useState([]);
const darkMode = useSelector(state => state.theme.darkMode);
- const fetchChartData = async () => {
- try {
- const response = await axios.get(`${process.env.REACT_APP_APIENDPOINT}/tools/utilization`, {
- params: {
- startDate,
- endDate,
- tool: toolFilter,
- project: projectFilter,
- },
- headers: {
- Authorization: localStorage.getItem('token'),
- },
- });
- setToolsData(response.data);
- } catch (err) {
- setError('Failed to load utilization data.');
- }
- };
+ const { toolsData, loading: chartLoading, error: chartError, fetchData } = useUtilizationData();
+ const {
+ insights,
+ loading: insightsLoading,
+ error: insightsError,
+ fetchInsights,
+ } = useUtilizationInsights();
- const fetchFilterData = async () => {
+ const fetchFilterData = useCallback(async () => {
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'),
- },
+ axios.get(`${ENDPOINTS.BM_PROJECTS}Names`, {
+ headers: { Authorization: localStorage.getItem('token') },
}),
]);
setToolTypes(toolTypesResponse.data);
setProjects(projectsResponse.data);
- } catch (err) {
- setError('Failed to load filter options. Please try refreshing the page.');
+ } catch {
+ toast.error('Failed to load filter options. Please try refreshing the page.');
}
- };
+ }, []);
useEffect(() => {
fetchFilterData();
- fetchChartData();
- }, []);
+ fetchData({ tool: 'ALL', project: 'ALL', mode: FORECAST_MODES.HISTORICAL });
+ fetchInsights({ tool: 'ALL', project: 'ALL' });
+ }, [fetchFilterData, fetchData, fetchInsights]);
+
+ const buildParams = useCallback(
+ () => ({
+ tool: toolFilter,
+ project: projectFilter,
+ ...(startDate && { startDate: startDate.toISOString() }),
+ ...(endDate && { endDate: endDate.toISOString() }),
+ }),
+ [toolFilter, projectFilter, startDate, endDate],
+ );
const handleApplyClick = () => {
- fetchChartData();
+ const params = buildParams();
+ fetchData({ ...params, mode: forecastMode });
+ fetchInsights(params);
};
- const chartData = {
- labels: toolsData.map(tool => tool.name),
- datasets: [
- {
- label: 'Utilization (%)',
- data: toolsData.map(tool => tool.utilizationRate),
- backgroundColor: darkMode ? '#007bff' : '#a0e7e5',
- borderRadius: 6,
- },
- ],
- };
+ const handleForecastModeChange = useCallback(
+ newMode => {
+ setForecastMode(newMode);
+ const params = buildParams();
+ fetchData({ ...params, mode: newMode });
+ },
+ [buildParams, fetchData],
+ );
- const options = {
- indexAxis: 'y',
- responsive: true,
- plugins: {
- legend: {
- labels: { color: darkMode ? '#ffffff' : '#333' },
- },
- datalabels: {
- color: darkMode ? '#ffffff' : '#333',
- anchor: 'end',
- align: 'end',
- font: {
- size: 12,
- },
- formatter: (_, context) => {
- const tool = toolsData[context.dataIndex];
- return `${tool.downtime} hrs`;
+ const warningMessage = useMemo(() => toolsData.find(row => row.warning)?.warning || null, [
+ toolsData,
+ ]);
+
+ const chartAnnouncement = useMemo(() => {
+ if (chartLoading) return 'Loading utilization data.';
+ if (chartError) return `Error: ${chartError}`;
+ if (toolsData.length === 0) return 'No utilization data available.';
+ return `Chart updated. Showing ${toolsData.length} tool types.`;
+ }, [chartLoading, chartError, toolsData]);
+
+ const chartData = useMemo(
+ () => ({
+ labels: toolsData.map(tool => tool.name),
+ datasets: [
+ {
+ label: 'Utilization (%)',
+ data: toolsData.map(tool => tool.utilizationRate),
+ backgroundColor: toolsData.map(tool => getBarColor(tool.classification?.trafficLight)),
+ borderRadius: 6,
},
+ ...(forecastMode !== FORECAST_MODES.HISTORICAL
+ ? [
+ {
+ label: 'Predicted Utilization (%)',
+ data: toolsData.map(tool => tool.forecast?.predictedRate ?? null),
+ backgroundColor: toolsData.map(tool =>
+ tool.forecast?.predictedClassification
+ ? `${
+ TRAFFIC_LIGHT_COLORS[tool.forecast.predictedClassification.trafficLight]
+ }80`
+ : '#94a3b880',
+ ),
+ borderRadius: 6,
+ borderWidth: 2,
+ borderColor: toolsData.map(tool =>
+ tool.forecast?.predictedClassification
+ ? TRAFFIC_LIGHT_COLORS[tool.forecast.predictedClassification.trafficLight]
+ : '#94a3b8',
+ ),
+ },
+ ]
+ : []),
+ ],
+ }),
+ [toolsData, forecastMode],
+ );
+
+ const trafficLightPlugin = useMemo(
+ () => ({
+ id: 'trafficLightIndicators',
+ afterDraw: chart => {
+ const { ctx } = chart;
+ const yAxis = chart.scales.y;
+ toolsData.forEach((tool, index) => {
+ const yPos = yAxis.getPixelForTick(index);
+ const color = getBarColor(tool.classification?.trafficLight);
+ ctx.beginPath();
+ ctx.arc(yAxis.left - 12, yPos, 5, 0, 2 * Math.PI);
+ ctx.fillStyle = color;
+ ctx.fill();
+ });
},
- tooltip: {
- callbacks: {
- label: context => {
+ }),
+ [toolsData],
+ );
+
+ const options = useMemo(
+ () => ({
+ indexAxis: 'y',
+ responsive: true,
+ layout: { padding: { left: 20 } },
+ plugins: {
+ legend: {
+ labels: { color: darkMode ? '#ffffff' : '#333' },
+ },
+ datalabels: {
+ color: darkMode ? '#ffffff' : '#333',
+ anchor: 'end',
+ align: 'end',
+ font: { size: 12 },
+ formatter: (value, context) => {
+ if (context.datasetIndex === 1) {
+ const tool = toolsData[context.dataIndex];
+ return tool?.forecast ? `\u2192 ${tool.forecast.predictedRate}%` : '';
+ }
const tool = toolsData[context.dataIndex];
- return `Utilization: ${tool.utilizationRate}%, Downtime: ${tool.downtime} hrs`;
+ if (!tool) return '';
+ const classLabel = tool.classification?.label || '';
+ return classLabel
+ ? `${tool.downtime} hrs \u00B7 ${classLabel}`
+ : `${tool.downtime} hrs`;
},
},
- footerColor: 'white',
- },
- },
- scales: {
- x: {
- max: 100,
- title: {
- display: true,
- text: 'Time (%)',
- color: darkMode ? '#ffffff' : '#333',
+ tooltip: {
+ callbacks: {
+ label: context => {
+ const tool = toolsData[context.dataIndex];
+ if (!tool) return '';
+ if (context.datasetIndex === 0) {
+ return `Utilization: ${tool.utilizationRate}% (${tool.classification?.label ||
+ 'N/A'})`;
+ }
+ if (context.datasetIndex === 1 && tool.forecast) {
+ return `Predicted: ${tool.forecast.predictedRate}% (${tool.forecast.confidence} confidence)`;
+ }
+ return '';
+ },
+ afterLabel: context => {
+ const tool = toolsData[context.dataIndex];
+ if (context.datasetIndex === 0 && tool) {
+ return `Downtime: ${tool.downtime} hrs`;
+ }
+ return '';
+ },
+ },
},
- ticks: { color: darkMode ? '#ffffff' : '#333' },
- grid: { color: darkMode ? '#c7c7c7ff' : '#bebebeff' },
},
- y: {
- ticks: {
- autoSkip: false,
- color: darkMode ? '#ffffff' : '#333',
+ scales: {
+ x: {
+ max: 100,
+ title: {
+ display: true,
+ text: 'Time (%)',
+ color: darkMode ? '#ffffff' : '#333',
+ },
+ ticks: { color: darkMode ? '#ffffff' : '#333' },
+ grid: { color: darkMode ? '#c7c7c7' : '#bebebe' },
+ },
+ y: {
+ ticks: {
+ autoSkip: false,
+ color: darkMode ? '#ffffff' : '#333',
+ },
+ grid: { color: darkMode ? '#c7c7c7' : '#bebebe' },
},
- grid: { color: darkMode ? '#c7c7c7ff' : '#bebebeff' },
},
- },
- };
+ }),
+ [darkMode, toolsData],
+ );
return (
-
Utilization Chart
-
- {error ? (
-
{error}
- ) : (
- <>
-
-
-
-
-
- setStartDate(date)}
- placeholderText="Start Date"
- maxDate={endDate || '' || new Date()}
- className={styles.datepickerWrapper}
- />
-
- setEndDate(date)}
- placeholderText="End Date"
- minDate={startDate || ''}
- maxDate={new Date()}
- className={styles.datepickerWrapper}
- />
-
-
-
-
-
- >
+
Tool Utilization Analysis
+
+
+
+
+
+
+ setStartDate(date)}
+ placeholderText="Start Date"
+ maxDate={endDate || new Date()}
+ className={styles.datepickerWrapper}
+ aria-label="Start date"
+ />
+
+ setEndDate(date)}
+ placeholderText="End Date"
+ minDate={startDate || undefined}
+ maxDate={new Date()}
+ className={styles.datepickerWrapper}
+ aria-label="End date"
+ />
+
+
+
+
+
+
+ {forecastMode === FORECAST_MODES.FORECAST_FULL && warningMessage && (
+
+ {warningMessage}
+
+ )}
+
+ {!insightsLoading && insights.summary &&
}
+
+ {chartLoading && (
+
+
+
Loading utilization data...
+
+ )}
+
+ {!chartLoading && chartError && (
+
+ {chartError}
+
)}
+
+ {!chartLoading && !chartError && toolsData.length === 0 && (
+
+ No utilization data available for the selected filters.
+
+ )}
+
+ {!chartLoading && !chartError && toolsData.length > 0 && (
+
+
+
+ )}
+
+
+ {chartAnnouncement}
+
+
+ {!insightsLoading && !insightsError && (
+
+
+
+
+
+ )}
+
+
);
}
diff --git a/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css b/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css
index 43ca101cb8..dae7b95cfc 100644
--- a/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css
+++ b/src/components/BMDashboard/UtilizationChart/UtilizationChart.module.css
@@ -1,12 +1,19 @@
+/* ============ BASE CONTAINER & VARIABLES ============ */
.utilizationChartContainer {
padding: 2rem;
- max-width: 1000px;
+ max-width: 1200px;
margin: 0 auto;
background-color: var(--bg-color, #fff);
color: var(--text-color, #333);
border-radius: 10px;
box-shadow: var(--shadow, 0 2px 10px rgb(0 0 0 / 10%));
min-height: 100vh;
+
+ --card-bg: #f9fafb;
+ --text-secondary: #6b7280;
+ --hover-bg: #f3f4f6;
+ --panel-bg: #fff;
+ --panel-border: #e5e7eb;
}
.chartTitle {
@@ -15,6 +22,7 @@
color: var(--title-color, #333);
}
+/* ============ FILTERS ============ */
.filters {
display: flex;
flex-wrap: wrap;
@@ -44,7 +52,6 @@
border-color: var(--focus-border-color, #3b82f6);
}
-/* Force light dropdown in light mode (override any global dark select/option styles) */
.utilizationChartContainer:not(.darkMode) .select {
background-color: #fff !important;
color: #000 !important;
@@ -60,7 +67,6 @@
color: #999;
}
-/* Force light calendar when app is in light mode (datepicker renders in portal, so scope by body) */
:global(body:not(.dark-mode)) :global(.react-datepicker) {
background-color: #fff;
color: #333;
@@ -102,11 +108,379 @@
background-color: var(--button-hover, #333);
}
+/* ============ ERROR / EMPTY / LOADING STATES ============ */
.utilizationChartError {
color: var(--error-color, red);
text-align: center;
}
+.emptyMessage {
+ text-align: center;
+ color: var(--text-secondary, #6b7280);
+ padding: 2rem;
+ font-style: italic;
+}
+
+.loadingContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem 0;
+}
+
+.spinner {
+ width: 36px;
+ height: 36px;
+ border: 4px solid var(--panel-border, #e5e7eb);
+ border-top-color: var(--button-bg, #000);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.srOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* ============ FORECAST MODE TOGGLE ============ */
+.forecastToggle {
+ display: flex;
+ border: 1px solid var(--border-color, #ccc);
+ border-radius: 6px;
+ overflow: hidden;
+ margin-bottom: 1.5rem;
+ justify-content: center;
+}
+
+.toggleButton {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ border: none;
+ background-color: var(--input-bg, #fff);
+ color: var(--text-color, #333);
+ cursor: pointer;
+ transition: background-color 0.2s ease, color 0.2s ease;
+ white-space: nowrap;
+}
+
+.toggleButton:not(:last-child) {
+ border-right: 1px solid var(--border-color, #ccc);
+}
+
+.toggleButtonActive {
+ background-color: var(--button-bg, #000);
+ color: var(--button-text, #fff);
+ font-weight: 600;
+}
+
+.toggleButton:focus-visible {
+ outline: 2px solid var(--focus-border-color, #3b82f6);
+ outline-offset: -2px;
+}
+
+.toggleButton:hover:not(.toggleButtonActive) {
+ background-color: var(--hover-bg, #f3f4f6);
+}
+
+/* ============ WARNING BANNER ============ */
+.warningBanner {
+ background-color: #fef3c7;
+ border: 1px solid #fde68a;
+ border-radius: 6px;
+ padding: 0.75rem 1rem;
+ margin-bottom: 1rem;
+ color: #92400e;
+ font-size: 0.875rem;
+}
+
+/* ============ SUMMARY BAR ============ */
+.summaryBar {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ flex-wrap: wrap;
+}
+
+.summaryCard {
+ flex: 1;
+ min-width: 120px;
+ padding: 1rem;
+ border-radius: 8px;
+ background-color: var(--card-bg, #f9fafb);
+ border: 1px solid var(--panel-border, #e5e7eb);
+ text-align: center;
+}
+
+.summaryValue {
+ display: block;
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-color, #111);
+}
+
+.summaryLabel {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--text-secondary, #6b7280);
+ margin-top: 0.25rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.summaryCardGreen {
+ border-bottom: 3px solid #22c55e;
+}
+
+.summaryCardYellow {
+ border-bottom: 3px solid #eab308;
+}
+
+.summaryCardRed {
+ border-bottom: 3px solid #ef4444;
+}
+
+/* ============ INSIGHTS PANELS GRID ============ */
+.insightsPanelsGrid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+ margin-top: 1.5rem;
+}
+
+/* ============ SHARED PANEL STYLES ============ */
+.insightsPanel {
+ background-color: var(--panel-bg, #fff);
+ border: 1px solid var(--panel-border, #e5e7eb);
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.panelTitle {
+ font-size: 1rem;
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+ color: var(--text-color, #333);
+}
+
+.emptyPanel {
+ color: var(--text-secondary, #6b7280);
+ font-size: 0.875rem;
+ font-style: italic;
+}
+
+/* ============ RECOMMENDATION PANEL ============ */
+.recommendationList {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.recommendationItem {
+ display: flex;
+ gap: 0.75rem;
+ align-items: flex-start;
+ padding: 0.75rem 0;
+ border-bottom: 1px solid var(--panel-border, #e5e7eb);
+}
+
+.recommendationItem:last-child {
+ border-bottom: none;
+}
+
+.trafficDot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ margin-top: 0.35rem;
+}
+
+.toolName {
+ font-size: 0.875rem;
+ color: var(--text-color, #333);
+}
+
+.classificationBadge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ margin-left: 0.5rem;
+}
+
+.classificationBadge[data-level='green'] {
+ background-color: #dcfce7;
+ color: #166534;
+}
+
+.classificationBadge[data-level='yellow'] {
+ background-color: #fef9c3;
+ color: #854d0e;
+}
+
+.classificationBadge[data-level='red'] {
+ background-color: #fee2e2;
+ color: #991b1b;
+}
+
+.actionText {
+ font-size: 0.8125rem;
+ color: var(--text-secondary, #6b7280);
+ margin: 0.25rem 0;
+}
+
+.rateText {
+ font-size: 0.75rem;
+ color: var(--text-secondary, #6b7280);
+}
+
+/* ============ MAINTENANCE ALERTS ============ */
+.alertList {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.alertItem {
+ padding: 0.75rem;
+ border-radius: 6px;
+ border-left: 4px solid;
+ margin-bottom: 0.5rem;
+}
+
+.alertItem[data-urgency='high'] {
+ border-left-color: #ef4444;
+ background-color: #fef2f2;
+}
+
+.alertItem[data-urgency='medium'] {
+ border-left-color: #f59e0b;
+ background-color: #fffbeb;
+}
+
+.alertHeader {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.35rem;
+}
+
+.urgencyBadge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 0.6875rem;
+ font-weight: 700;
+ color: #fff;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.alertMessage {
+ font-size: 0.8125rem;
+ color: var(--text-secondary, #6b7280);
+ margin: 0;
+}
+
+/* ============ RESOURCE BALANCING ============ */
+.balancingList {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.balancingItem {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid var(--panel-border, #e5e7eb);
+}
+
+.balancingItem:last-child {
+ border-bottom: none;
+}
+
+.suggestionText {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--text-color, #333);
+ margin: 0 0 0.25rem;
+}
+
+.balancingDetail {
+ display: flex;
+ gap: 0.75rem;
+ font-size: 0.8125rem;
+ color: var(--text-color, #333);
+ margin-bottom: 0.25rem;
+}
+
+.fromTool,
+.toTool {
+ font-size: 0.8125rem;
+}
+
+.rationaleText {
+ font-size: 0.75rem;
+ color: var(--text-secondary, #6b7280);
+ margin: 0;
+ font-style: italic;
+}
+
+/* ============ EXPORT SECTION ============ */
+.exportSection {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 1.5rem;
+ justify-content: flex-end;
+}
+
+.exportLabel {
+ font-size: 0.875rem;
+ color: var(--text-secondary, #6b7280);
+}
+
+.exportButton {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ border-radius: 5px;
+ border: 1px solid var(--border-color, #ccc);
+ background-color: var(--input-bg, #fff);
+ color: var(--text-color, #333);
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.exportButton:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.exportButton:focus-visible {
+ outline: 2px solid var(--focus-border-color, #3b82f6);
+ outline-offset: -2px;
+}
+
+.exportButton:hover:not(:disabled) {
+ background-color: var(--button-bg, #000);
+ color: var(--button-text, #fff);
+}
+
/* ============ DARK MODE ============ */
.utilizationChartContainer.darkMode {
--bg-color: #1b2a41;
@@ -120,13 +494,17 @@
--button-hover: #f5b13a;
--shadow: 0 2px 10px rgb(255 255 255 / 10%);
--error-color: #ff6b6b;
+ --card-bg: #2b3e59;
+ --text-secondary: #94a3b8;
+ --hover-bg: #334155;
+ --panel-bg: #253342;
+ --panel-border: #4a5a77;
}
.utilizationChartContainer.darkMode .datepickerWrapper::placeholder {
color: #fff;
}
-/* React-DatePicker Dark Mode Dropdown Styling */
.utilizationChartContainer.darkMode :global(.react-datepicker) {
background-color: #333;
color: #e0e0e0;
@@ -154,3 +532,80 @@
background-color: #e8a71c;
color: #000;
}
+
+.darkMode .warningBanner {
+ background-color: #713f12;
+ border-color: #92400e;
+ color: #fef08a;
+}
+
+.darkMode .classificationBadge[data-level='green'] {
+ background-color: #14532d;
+ color: #bbf7d0;
+}
+
+.darkMode .classificationBadge[data-level='yellow'] {
+ background-color: #713f12;
+ color: #fef08a;
+}
+
+.darkMode .classificationBadge[data-level='red'] {
+ background-color: #7f1d1d;
+ color: #fecaca;
+}
+
+.darkMode .alertItem[data-urgency='high'] {
+ border-left-color: #ef4444;
+ background-color: #451a1a;
+}
+
+.darkMode .alertItem[data-urgency='medium'] {
+ border-left-color: #f59e0b;
+ background-color: #3b2f10;
+}
+
+/* ============ RESPONSIVE ============ */
+@media (width <= 1024px) {
+ .insightsPanelsGrid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (width <= 768px) {
+ .insightsPanelsGrid {
+ grid-template-columns: 1fr;
+ }
+
+ .summaryBar {
+ gap: 0.5rem;
+ }
+
+ .summaryCard {
+ min-width: 80px;
+ }
+
+ .filters {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
+
+@media (width <= 576px) {
+ .utilizationChartContainer {
+ padding: 1rem;
+ }
+
+ .forecastToggle {
+ flex-direction: column;
+ }
+
+ .toggleButton:not(:last-child) {
+ border-right: none;
+ border-bottom: 1px solid var(--border-color, #ccc);
+ }
+
+ .exportSection {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
diff --git a/src/components/BMDashboard/UtilizationChart/constants.js b/src/components/BMDashboard/UtilizationChart/constants.js
new file mode 100644
index 0000000000..961a7f3bbe
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/constants.js
@@ -0,0 +1,45 @@
+export const FORECAST_MODES = {
+ HISTORICAL: 'historical',
+ FORECAST_30: 'forecast30',
+ FORECAST_FULL: 'forecastFull',
+};
+
+export const FORECAST_MODE_LABELS = {
+ [FORECAST_MODES.HISTORICAL]: 'Historical View',
+ [FORECAST_MODES.FORECAST_30]: 'Next 30 Days',
+ [FORECAST_MODES.FORECAST_FULL]: 'Full Project Schedule',
+};
+
+export const TRAFFIC_LIGHT_COLORS = {
+ green: '#22c55e',
+ yellow: '#eab308',
+ red: '#ef4444',
+};
+
+export const TRAFFIC_LIGHT_LABELS = {
+ green: 'Normal',
+ yellow: 'Under-utilized',
+ red: 'Over-utilized',
+};
+
+export const CLASSIFICATION_LABELS = {
+ UNDER: 'Under-utilized',
+ NORMAL: 'Normal',
+ OVER: 'Over-utilized',
+};
+
+export const URGENCY_STYLES = {
+ high: { color: '#ef4444', label: 'HIGH' },
+ medium: { color: '#f59e0b', label: 'MEDIUM' },
+};
+
+export const CONFIDENCE_STYLES = {
+ low: { color: '#ef4444', label: 'Low confidence' },
+ medium: { color: '#f59e0b', label: 'Medium confidence' },
+ high: { color: '#22c55e', label: 'High confidence' },
+};
+
+export const EXPORT_FORMATS = {
+ PDF: 'pdf',
+ CSV: 'csv',
+};
diff --git a/src/components/BMDashboard/UtilizationChart/hooks/useUtilizationData.js b/src/components/BMDashboard/UtilizationChart/hooks/useUtilizationData.js
new file mode 100644
index 0000000000..75c572a9bf
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/hooks/useUtilizationData.js
@@ -0,0 +1,48 @@
+import { useState, useCallback, useRef } from 'react';
+import axios, { isCancel } from 'axios';
+import { ENDPOINTS } from '../../../../utils/URL';
+
+export function useUtilizationData() {
+ const [toolsData, setToolsData] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const abortControllerRef = useRef(null);
+
+ const fetchData = useCallback(async (params = {}) => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ const controller = new AbortController();
+ abortControllerRef.current = controller;
+
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await axios.get(ENDPOINTS.BM_TOOL_UTILIZATION, {
+ params: {
+ tool: params.tool,
+ project: params.project,
+ startDate: params.startDate,
+ endDate: params.endDate,
+ mode: params.mode,
+ },
+ headers: { Authorization: localStorage.getItem('token') },
+ signal: controller.signal,
+ });
+ setToolsData(response.data);
+ } catch (err) {
+ if (isCancel(err)) return;
+ const message =
+ err.response?.status === 400 && err.response?.data?.error
+ ? err.response.data.error
+ : 'Failed to load utilization data.';
+ setError(message);
+ } finally {
+ if (!controller.signal.aborted) {
+ setLoading(false);
+ }
+ }
+ }, []);
+
+ return { toolsData, loading, error, fetchData };
+}
diff --git a/src/components/BMDashboard/UtilizationChart/hooks/useUtilizationInsights.js b/src/components/BMDashboard/UtilizationChart/hooks/useUtilizationInsights.js
new file mode 100644
index 0000000000..c7bad7a6b4
--- /dev/null
+++ b/src/components/BMDashboard/UtilizationChart/hooks/useUtilizationInsights.js
@@ -0,0 +1,54 @@
+import { useState, useCallback, useRef } from 'react';
+import axios, { isCancel } from 'axios';
+import { ENDPOINTS } from '../../../../utils/URL';
+
+const INITIAL_INSIGHTS = {
+ recommendations: [],
+ maintenanceAlerts: [],
+ resourceBalancing: [],
+ summary: null,
+};
+
+export function useUtilizationInsights() {
+ const [insights, setInsights] = useState(INITIAL_INSIGHTS);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const abortControllerRef = useRef(null);
+
+ const fetchInsights = useCallback(async (params = {}) => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ const controller = new AbortController();
+ abortControllerRef.current = controller;
+
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await axios.get(ENDPOINTS.BM_TOOL_UTILIZATION_INSIGHTS, {
+ params: {
+ tool: params.tool,
+ project: params.project,
+ startDate: params.startDate,
+ endDate: params.endDate,
+ },
+ headers: { Authorization: localStorage.getItem('token') },
+ signal: controller.signal,
+ });
+ setInsights(response.data);
+ } catch (err) {
+ if (isCancel(err)) return;
+ const message =
+ err.response?.status === 400 && err.response?.data?.error
+ ? err.response.data.error
+ : 'Failed to load insights data.';
+ setError(message);
+ } finally {
+ if (!controller.signal.aborted) {
+ setLoading(false);
+ }
+ }
+ }, []);
+
+ return { insights, loading, error, fetchInsights };
+}
diff --git a/src/utils/URL.js b/src/utils/URL.js
index 6ab4ebf4e7..89a68907d1 100644
--- a/src/utils/URL.js
+++ b/src/utils/URL.js
@@ -347,6 +347,9 @@ export const ENDPOINTS = {
BM_TOOL_AVAILABILITY: (toolId = '', projectId = '') =>
`${APIEndpoint}/tools/availability?toolId=${toolId}&projectId=${projectId}`,
BM_LOG_TOOLS: `${APIEndpoint}/bm/tools/log`,
+ BM_TOOL_UTILIZATION: `${APIEndpoint}/tools/utilization`,
+ BM_TOOL_UTILIZATION_INSIGHTS: `${APIEndpoint}/tools/utilization/insights`,
+ BM_TOOL_UTILIZATION_EXPORT: `${APIEndpoint}/tools/utilization/export`,
BM_EQUIPMENT_BY_ID: singleEquipmentId => `${APIEndpoint}/bm/equipment/${singleEquipmentId}`,
BM_EQUIPMENT_STATUS_UPDATE: (equipmentId) => `${APIEndpoint}/bm/equipment/${equipmentId}/status`,
BM_EQUIPMENTS: `${APIEndpoint}/bm/equipments`,