diff --git a/src/components/BMDashboard/Tools/ToolsStoppageHorizontalBarChart.jsx b/src/components/BMDashboard/Tools/ToolsStoppageHorizontalBarChart.jsx
new file mode 100644
index 0000000000..aa24bb49ae
--- /dev/null
+++ b/src/components/BMDashboard/Tools/ToolsStoppageHorizontalBarChart.jsx
@@ -0,0 +1,380 @@
+import { useState, useEffect } from 'react';
+import { useSelector } from 'react-redux';
+import Select from 'react-select';
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+import { Row, Col, Button } from 'react-bootstrap';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip as ChartTooltip,
+ Legend as ChartLegend,
+} from 'chart.js';
+import ChartDataLabels from 'chartjs-plugin-datalabels';
+import { Bar } from 'react-chartjs-2';
+import httpService from '../../../services/httpService';
+import logService from '../../../services/logService';
+import { ENDPOINTS } from '../../../utils/URL';
+import { getStandardSelectStyles } from '../../../utils/reactSelectUtils';
+import {
+ getChartHeight,
+ getMaxBarThickness,
+ getCategoryPercentage,
+ getBarPercentage,
+ getChartFontSize,
+ getChartTitleFontSize,
+} from '../../../utils/chartResponsiveUtils';
+import styles from './ToolsStoppageHorizontalBarChart.module.css';
+
+// Register Chart.js components
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ ChartTooltip,
+ ChartLegend,
+ ChartDataLabels,
+);
+
+export default function ToolsStoppageHorizontalBarChart() {
+ const darkMode = useSelector(state => state.theme.darkMode);
+ const [projects, setProjects] = useState([]);
+ const [selectedProject, setSelectedProject] = useState(null);
+ const [dateRange, setDateRange] = useState([null, null]);
+ const [startDate, endDate] = dateRange;
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [data, setData] = useState([]);
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
+ const emptyData = [];
+
+ useEffect(() => {
+ const handleResize = () => setWindowWidth(window.innerWidth);
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ useEffect(() => {
+ const fetchProjects = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await httpService.get(ENDPOINTS.BM_TOOL_PROJECTS);
+ const responseData = response.data;
+
+ // Handle new structured response format
+ if (responseData.success === false) {
+ setError(responseData.message || 'Failed to load projects.');
+ setProjects([]);
+ return;
+ }
+
+ // Extract data array from structured response
+ const projectsData = responseData.data || responseData;
+ setProjects(Array.isArray(projectsData) ? projectsData : []);
+ } catch (err) {
+ logService.logError(err);
+ if (err.response?.data?.message) {
+ setError(err.response.data.message);
+ } else if (err.response?.status === 401 || err.response?.status === 403) {
+ setError('Session expired. Please log in again.');
+ } else if (!err.response) {
+ setError('Network error. Please check your connection.');
+ } else if (err.response?.status >= 500) {
+ setError('Server error. Please try again later.');
+ } else {
+ setError(
+ `Failed to load projects. Please try again. (Status: ${err.response?.status ||
+ 'unknown'})`,
+ );
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchProjects();
+ }, []);
+
+ // Auto-select first project when projects load
+ useEffect(() => {
+ if (!selectedProject && projects.length > 0) {
+ const firstProject = projects[0];
+ setSelectedProject({
+ value: firstProject.projectId,
+ label: firstProject.projectName,
+ });
+ }
+ }, [projects, selectedProject]);
+
+ // Fetch tools stoppage data when project or date filters change
+ useEffect(() => {
+ const fetchToolsStoppageData = async () => {
+ // Early return if no project selected
+ if (!selectedProject) {
+ setData(emptyData);
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+ const formattedStart = startDate ? new Date(startDate).toISOString() : null;
+ const formattedEnd = endDate ? new Date(endDate).toISOString() : null;
+
+ try {
+ const url = ENDPOINTS.BM_TOOLS_STOPPAGE_BY_PROJECT(
+ selectedProject.value,
+ formattedStart,
+ formattedEnd,
+ );
+ const response = await httpService.get(url);
+ const responseData = response.data;
+
+ // Handle new structured response format
+ if (responseData.success === false) {
+ setError(responseData.message || 'Failed to load stoppage data.');
+ setData(emptyData);
+ return;
+ }
+
+ // Extract data array from structured response
+ const stoppageData = responseData.data || responseData;
+
+ if (stoppageData && Array.isArray(stoppageData) && stoppageData.length > 0) {
+ const sortedData = [...stoppageData].map(item => ({
+ ...item,
+ name: item.toolName || item.name,
+ }));
+ setData(sortedData);
+ } else {
+ setData(emptyData);
+ // Use message from API if available
+ const message =
+ responseData.message || 'No tool stoppage reason data found for this project.';
+ setError(message);
+ }
+ } catch (err) {
+ logService.logError(err);
+ setData(emptyData);
+
+ // Enhanced error handling
+ if (err.response?.data?.message) {
+ setError(err.response.data.message);
+ } else if (err.response?.status === 401 || err.response?.status === 403) {
+ setError('Session expired. Please log in again.');
+ } else if (!err.response) {
+ setError('Network error. Please check your connection.');
+ } else if (err.response?.status >= 500) {
+ setError('Server error. Please try again later.');
+ } else {
+ setError(
+ `Failed to load tools stoppage reason data. Please try again. (Status: ${err.response
+ ?.status || 'unknown'})`,
+ );
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchToolsStoppageData();
+ }, [selectedProject, startDate, endDate]);
+
+ const projectOptions = projects.map(project => ({
+ value: project.projectId,
+ label: project.projectName,
+ }));
+
+ // Format date for display
+ const formatDate = date => date?.toISOString().split('T')[0];
+ const dateRangeLabel =
+ startDate && endDate ? `${formatDate(startDate)} - ${formatDate(endDate)}` : '';
+
+ // Use shared react-select styles to reduce duplication
+ const selectStyles = getStandardSelectStyles(darkMode);
+
+ // Prepare Chart.js data with responsive bar thickness
+ const chartData = {
+ labels: data.map(item =>
+ item.name.length > 20 ? `${item.name.substring(0, 18)}...` : item.name,
+ ),
+ datasets: [
+ {
+ label: 'Used its lifetime',
+ data: data.map(item => item.usedForLifetime || 0),
+ backgroundColor: '#4589FF',
+ maxBarThickness: getMaxBarThickness(windowWidth),
+ },
+ {
+ label: 'Damaged',
+ data: data.map(item => item.damaged || 0),
+ backgroundColor: '#FF0000',
+ maxBarThickness: getMaxBarThickness(windowWidth),
+ },
+ {
+ label: 'Lost',
+ data: data.map(item => item.lost || 0),
+ backgroundColor: '#FFB800',
+ maxBarThickness: getMaxBarThickness(windowWidth),
+ },
+ ],
+ };
+
+ // Chart.js options for horizontal stacked bars with responsive settings
+ const chartOptions = {
+ indexAxis: 'y',
+ maintainAspectRatio: false,
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'top',
+ labels: {
+ color: darkMode ? '#e0e0e0' : '#000',
+ font: { size: getChartFontSize(windowWidth) },
+ },
+ },
+ tooltip: { enabled: true, color: darkMode ? '#FFFFFF' : '#000000' },
+ datalabels: {
+ display: true,
+ color: '#fff',
+ font: { weight: 'bold', size: getChartFontSize(windowWidth) },
+ },
+ },
+ scales: {
+ x: {
+ stacked: true,
+ grid: { color: darkMode ? '#364156' : '#e0e0e0' },
+ border: {
+ color: darkMode ? '#ffffff' : '#000000', // Make axis border visible in dark mode
+ width: 1,
+ },
+ ticks: {
+ color: darkMode ? '#ffffff' : '#000', // Brighter color in dark mode for better visibility
+ font: { size: getChartFontSize(windowWidth) },
+ maxRotation: 0,
+ },
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Tools',
+ color: darkMode ? '#FFFFFF' : '#000000',
+ font: { size: getChartTitleFontSize(windowWidth) },
+ },
+ stacked: true,
+ grid: { display: false },
+ border: {
+ color: darkMode ? '#ffffff' : '#000000', // Make axis border visible in dark mode
+ width: 1,
+ },
+ ticks: {
+ color: darkMode ? '#ffffff' : '#000', // Brighter color in dark mode for better visibility
+ font: { size: getChartFontSize(windowWidth) },
+ },
+ categoryPercentage: getCategoryPercentage(windowWidth),
+ barPercentage: getBarPercentage(windowWidth),
+ },
+ },
+ };
+
+ return (
+
+
+ Reason of Stoppage of Tools
+
+
+
+
+
+
+ {
+ setDateRange(update);
+ }}
+ placeholderText={dateRangeLabel || 'Filter by Date Range'}
+ className={styles.datePickerInput}
+ calendarClassName={darkMode ? 'darkThemeCalendar' : 'customCalendar'}
+ wrapperClassName={darkMode ? 'darkThemeDatePickerWrapper' : ''}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {error &&
{error}
}
+ {loading &&
Loading tool availability data...
}
+
+ {!loading && selectedProject && data.length > 0 && (
+
+
+
+ )}
+
+ {!loading && selectedProject && data.length === 0 && (
+
+
No data available for the selected filters.
+
+ )}
+
+
+ );
+}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.css b/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.css
new file mode 100644
index 0000000000..94c5cd0f24
--- /dev/null
+++ b/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.css
@@ -0,0 +1,288 @@
+.tool-donut-wrapper {
+ --donut-text-color: #000;
+ --legend-text-color: #000;
+ width: 100%;
+ max-width: 100%;
+ margin: 0 auto;
+ text-align: center;
+ height: auto; /* Ensure wrapper doesn't stretch */
+ display: flex;
+ flex-direction: column; /* Stack content vertically */
+ box-sizing: border-box;
+}
+
+.dark-mode.tool-donut-wrapper {
+ --donut-text-color: #fff;
+ --legend-text-color: #fff;
+}
+
+.tool-donut-title {
+ text-align: center;
+ font-weight: 600;
+ margin-bottom: 1.2rem;
+ font-size: 1.1rem;
+ color: var(--donut-text-color);
+ flex-shrink: 0;
+}
+
+.tool-donut-filters {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 3rem;
+ margin-bottom: 1.5rem;
+ flex-shrink: 0;
+}
+
+.filter-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 14px;
+ text-align: center;
+ min-width: 200px;
+ width: 100%;
+ max-width: 100%;
+}
+
+.filter-label {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--donut-text-color);
+ margin-bottom: 4px;
+ width: 100%;
+}
+
+.tool-donut-select {
+ width: 100%;
+ max-width: 100%;
+}
+
+.tool-donut-chart-container {
+ flex-shrink: 0;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.filter-value {
+ font-size: 14px;
+ color: #888;
+ margin-top: 2px;
+ font-weight: 500;
+}
+
+.tool-donut-legend {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-top: 1rem;
+ gap: 0.5rem;
+ width: 100%;
+ flex-shrink: 0;
+}
+
+.tool-donut-legend-item {
+ color: #ffffff;
+ padding: 6px 16px;
+ border-radius: 0;
+ font-size: 14px;
+ font-weight: 500;
+ min-width: 140px;
+ text-align: center;
+}
+
+/* Responsive Donut Layout */
+.tools-tracking-layout {
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+ justify-content: space-between;
+ align-items: stretch;
+ padding: 1rem;
+}
+
+.tools-donut-wrap {
+ flex: 0 0 60%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.tools-card-wrap {
+ flex: 0 0 40%;
+ background: #f9f9f9;
+ border-radius: 8px;
+ padding: 1rem;
+ font-size: 1rem;
+ font-weight: 500;
+ color: #333;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
+}
+
+/* Gradient responsive scaling for donut chart */
+@media (max-width: 375px) {
+ .tool-donut-wrapper {
+ max-width: 100%;
+ padding: 0;
+ }
+
+ .tool-donut-title {
+ font-size: 0.85rem;
+ margin-bottom: 0.6rem;
+ }
+
+ .tool-donut-filters {
+ gap: 1rem;
+ margin-bottom: 0.8rem;
+ }
+
+ .filter-item {
+ font-size: 11px;
+ }
+
+ .filter-label {
+ font-size: 11px;
+ }
+
+ .tool-donut-legend {
+ margin-top: 0.6rem;
+ gap: 0.3rem;
+ }
+
+ .tool-donut-legend-item {
+ font-size: 11px;
+ padding: 4px 10px;
+ min-width: 100px;
+ }
+}
+
+@media (min-width: 376px) and (max-width: 428px) {
+ .tool-donut-wrapper {
+ max-width: 100%;
+ padding: 0;
+ }
+
+ .tool-donut-title {
+ font-size: 0.9rem;
+ margin-bottom: 0.7rem;
+ }
+
+ .tool-donut-filters {
+ gap: 1.2rem;
+ margin-bottom: 0.9rem;
+ }
+
+ .filter-item {
+ font-size: 11.5px;
+ }
+
+ .filter-label {
+ font-size: 11.5px;
+ }
+
+ .tool-donut-legend {
+ margin-top: 0.7rem;
+ gap: 0.35rem;
+ }
+
+ .tool-donut-legend-item {
+ font-size: 11.5px;
+ padding: 4px 11px;
+ min-width: 110px;
+ }
+}
+
+@media (min-width: 429px) and (max-width: 480px) {
+ .tool-donut-wrapper {
+ max-width: 100%;
+ padding: 0;
+ }
+
+ .tool-donut-title {
+ font-size: 0.95rem;
+ margin-bottom: 0.75rem;
+ }
+
+ .tool-donut-filters {
+ gap: 1.3rem;
+ margin-bottom: 0.95rem;
+ }
+
+ .filter-item {
+ font-size: 11.5px;
+ }
+
+ .filter-label {
+ font-size: 11.5px;
+ }
+
+ .tool-donut-legend {
+ margin-top: 0.75rem;
+ gap: 0.35rem;
+ }
+
+ .tool-donut-legend-item {
+ font-size: 11.5px;
+ padding: 5px 11px;
+ min-width: 115px;
+ }
+}
+
+@media (min-width: 481px) and (max-width: 768px) {
+ .tools-tracking-layout {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .tools-donut-wrap,
+ .tools-card-wrap {
+ flex: 1 1 100%;
+ max-width: 100%;
+ }
+
+ .tool-donut-wrapper {
+ max-width: 100%;
+ padding: 0;
+ }
+
+ .tool-donut-title {
+ font-size: 1rem;
+ margin-bottom: 0.8rem;
+ }
+
+ .tool-donut-filters {
+ gap: 1.5rem;
+ margin-bottom: 1rem;
+ }
+
+ .filter-item {
+ font-size: 12px;
+ }
+
+ .filter-label {
+ font-size: 12px;
+ }
+
+ .tool-donut-legend {
+ margin-top: 0.8rem;
+ gap: 0.4rem;
+ }
+
+ .tool-donut-legend-item {
+ font-size: 12px;
+ padding: 5px 12px;
+ min-width: 120px;
+ }
+}
+
+/* Tablet adjustments */
+@media (min-width: 769px) and (max-width: 1024px) {
+ .tool-donut-wrapper {
+ max-width: 100%;
+ }
+
+ .tool-donut-filters {
+ gap: 2rem;
+ }
+}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx
index 4a595f35a9..7bf81f0bc3 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx
@@ -1,6 +1,7 @@
-import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
+import { useState, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
+import Select from 'react-select';
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts';
import { fetchToolAvailability, fetchTools } from '../../../../actions/bmdashboard/toolActions';
import styles from './ToolStatusDonutChart.module.css';
@@ -13,8 +14,8 @@ const COLORS = {
const RADIAN = Math.PI / 180;
const renderCustomizedLabel = ({ cx, cy, midAngle, outerRadius, percent, width }) => {
- const isSmall = width <= 768;
- if (isSmall) return null;
+ // Hide labels on mobile/tablet for better readability
+ if (width <= 1024) return null;
const radius = outerRadius + 20;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
@@ -162,62 +163,135 @@ export default function ToolStatusDonutChart() {
return () => window.removeEventListener('resize', handleResize);
}, []);
- const isXS = windowWidth <= 480;
const chartData = availabilityData?.data || [];
const total = availabilityData?.total || 0;
- // Check if we have no data for the selected combination
- const hasNoData = (toolId || projectId) && chartData.length === 0 && total === 0;
- const hasNoToolsMatch = total === 0;
-
- // Use the stored initial data for dropdowns, or fall back to current data
- const dropdownData = allToolsData || availabilityData;
- const toolsFromDropdown = dropdownData?.tools || [];
- const allAvailableTools =
- Array.isArray(toolsFromDropdown) && toolsFromDropdown.length
- ? toolsFromDropdown
- : toolslist || [];
-
- // Get all unique projects from the combined data
- const uniqueProjects = Array.from(
- new Map(
- allAvailableTools
- .filter(t => t?.projectId)
- .map(t => [t.projectId, { id: t.projectId, name: t.projectName || 'Unnamed Project' }]),
- ).values(),
+ // Extract unique projects from fetched projects list
+ const uniqueProjects = useMemo(
+ () =>
+ Array.from(
+ new Map(
+ projects
+ .filter(p => p?.projectId)
+ .map(p => [
+ p.projectId,
+ { id: p.projectId, name: p.projectName || p.projectId || 'Unnamed Project' },
+ ]),
+ ).values(),
+ ),
+ [projects],
);
- // Get all unique tools from the combined data
- const uniqueTools = Array.from(
- new Map(
- allAvailableTools
- .filter(t => t?.toolId)
- .map(t => [t.toolId, { id: t.toolId, name: t.name || 'Unnamed Tool' }]),
- ).values(),
+ // Extract unique tools from toolslist using correct data structure (tool.itemType._id and tool.itemType.name)
+ const uniqueTools = useMemo(
+ () =>
+ Array.from(
+ new Map(
+ toolslist
+ .filter(t => t?.itemType?._id && t?.itemType?.name)
+ .map(t => [t.itemType._id, { id: t.itemType._id, name: t.itemType.name }]),
+ ).values(),
+ ),
+ [toolslist],
);
- // Get the selected tool name
- const selectedTool = uniqueTools.find(tool => tool.id === toolId);
- const toolName = selectedTool ? selectedTool.name : null;
+ // Build react-select option lists
+ const projectOptions = useMemo(
+ () => [
+ { label: 'All', value: '' },
+ ...uniqueProjects.map(project => ({
+ label: project.name,
+ value: project.id,
+ })),
+ ],
+ [uniqueProjects],
+ );
+
+ const toolOptions = useMemo(
+ () => [
+ { label: 'All', value: '' },
+ ...uniqueTools.map(tool => ({
+ label: tool.name,
+ value: tool.id,
+ })),
+ ],
+ [uniqueTools],
+ );
+
+ // Use shared react-select styles to reduce duplication
+ const selectStyles = useMemo(() => getStandardSelectStyles(darkMode), [darkMode]);
+ // Gradient responsive sizing - matches other charts for consistent height
+ // Scales smoothly from smallest phones to desktop
let innerRadius;
let outerRadius;
let chartHeight;
- if (isXS) {
- innerRadius = 25;
- outerRadius = 40;
- chartHeight = 180;
- } else if (windowWidth <= 768) {
+ const isSmall = windowWidth <= 768;
+
+ if (windowWidth <= 375) {
+ // Small phones (iPhone SE, iPhone 12 mini)
innerRadius = 30;
outerRadius = 50;
- chartHeight = 200;
- } else {
+ chartHeight = 180;
+ } else if (windowWidth <= 428) {
+ // Medium phones (iPhone 12/13/14)
innerRadius = 35;
+ outerRadius = 55;
+ chartHeight = 200;
+ } else if (windowWidth <= 480) {
+ // Large phones
+ innerRadius = 37;
outerRadius = 60;
chartHeight = 220;
+ } else if (windowWidth <= 768) {
+ // Tablets in portrait
+ innerRadius = 40;
+ outerRadius = 65;
+ chartHeight = 240;
+ } else if (windowWidth <= 1024) {
+ // Tablets in landscape
+ innerRadius = 50;
+ outerRadius = 80;
+ chartHeight = 280;
+ } else {
+ // Desktop
+ innerRadius = 70;
+ outerRadius = 100;
+ chartHeight = 300;
}
const wrapperClass = `${styles.toolDonutWrapper} ${darkMode ? styles.toolDonutWrapperDark : ''}`;
+ // Gradient responsive margins scaling
+ const getChartMargins = () => {
+ if (windowWidth <= 375) {
+ return { top: 15, bottom: 15, left: 15, right: 15 };
+ } else if (windowWidth <= 428) {
+ return { top: 18, bottom: 18, left: 18, right: 18 };
+ } else if (windowWidth <= 480) {
+ return { top: 19, bottom: 19, left: 19, right: 19 };
+ } else if (windowWidth <= 768) {
+ return { top: 20, bottom: 20, left: 20, right: 20 };
+ } else if (windowWidth <= 1024) {
+ return { top: 25, bottom: 25, left: 30, right: 30 };
+ }
+ return { top: 30, bottom: 30, left: 40, right: 40 };
+ };
+
+ // Gradient responsive font size for center text scaling
+ const getCenterTextFontSize = () => {
+ if (windowWidth <= 375) {
+ return 8;
+ } else if (windowWidth <= 428) {
+ return 9;
+ } else if (windowWidth <= 480) {
+ return 9.5;
+ } else if (windowWidth <= 768) {
+ return 10;
+ } else if (windowWidth <= 1024) {
+ return 12;
+ }
+ return 14;
+ };
return (
@@ -227,32 +301,34 @@ export default function ToolStatusDonutChart() {
-
+
-
+
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.jsx
index 25d3101279..60e9f3713f 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.jsx
@@ -6,7 +6,6 @@ import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGri
import axios from 'axios';
import { ENDPOINTS } from '../../../../utils/URL';
import styles from './ToolsHorizontalBarChart.module.css';
-
function CustomTooltip({ active, payload, label, darkMode }) {
if (!active || !payload || !payload.length) {
return null;
@@ -71,6 +70,7 @@ function ToolsHorizontalBarChart({ darkMode: darkModeProp }) {
const [allTools, setAllTools] = useState([]);
const [selectedTools, setSelectedTools] = useState([]);
const [isPreviewHovering, setIsPreviewHovering] = useState(false);
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const currentDate = new Date();
const startDate12MonthsAgo = new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), 1);
@@ -79,6 +79,13 @@ function ToolsHorizontalBarChart({ darkMode: darkModeProp }) {
const [startDate, setStartDate] = useState(startDate12MonthsAgo.toISOString().split('T')[0]);
const [endDate, setEndDate] = useState(endOfCurrentMonth.toISOString().split('T')[0]);
+ useEffect(() => {
+ const handleResize = () => setWindowWidth(window.innerWidth);
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // Fetch projects list
useEffect(() => {
const fetchProjects = async () => {
try {
@@ -495,8 +502,11 @@ function ToolsHorizontalBarChart({ darkMode: darkModeProp }) {
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.module.css b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.module.css
index 5a1a60d11b..8e46a95859 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.module.css
+++ b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsHorizontalBarChart.module.css
@@ -5,12 +5,15 @@
background-color: var(--card-bg, #fff);
border-radius: 8px;
padding: 16px;
- height: 100%;
- box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
+ height: 100%; /* Fill parent card height - will match grid row height */
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
transition: transform 0.2s ease;
- min-height: 280px;
+ min-height: 0; /* Allow card to shrink to match donut chart height */
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
}
.tools-horizontal-bar-chart-card:hover {
@@ -38,10 +41,10 @@
}
.tools-horizontal-bar-chart-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-height: 0;
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ overflow: visible; /* Allow chart tooltips and labels to show */
}
.tools-horizontal-bar-chart-loading {
@@ -227,14 +230,129 @@
/* Responsive adjustments */
@media (width <= 768px) {
+/* Responsive adjustments - gradient scaling for mobile */
+@media (max-width: 375px) {
.tools-horizontal-bar-chart-card {
- padding: 12px;
- min-height: 250px;
+ padding: 6px;
+ min-height: auto;
+ max-height: none;
+ }
+
+ .tools-horizontal-bar-chart-title {
+ font-size: 12px;
+ margin-bottom: 6px;
+ }
+
+ .tools-horizontal-bar-chart-filters {
+ padding: 6px 0;
+ margin-bottom: 8px;
+ }
+
+ .tools-horizontal-bar-chart-date-picker-group {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ }
+
+ .tools-horizontal-bar-chart-date-picker {
+ min-width: 80px;
+ width: 100%;
+ font-size: 11px;
+ }
+
+ .tools-horizontal-bar-chart-loading,
+ .tools-horizontal-bar-chart-error,
+ .tools-horizontal-bar-chart-empty {
+ height: 150px;
+ }
+}
+
+@media (min-width: 376px) and (max-width: 428px) {
+ .tools-horizontal-bar-chart-card {
+ padding: 8px;
+ min-height: auto;
+ max-height: none;
+ }
+
+ .tools-horizontal-bar-chart-title {
+ font-size: 13px;
+ margin-bottom: 8px;
+ }
+
+ .tools-horizontal-bar-chart-filters {
+ padding: 7px 0;
+ margin-bottom: 9px;
+ }
+
+ .tools-horizontal-bar-chart-date-picker-group {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+ }
+
+ .tools-horizontal-bar-chart-date-picker {
+ min-width: 90px;
+ width: 100%;
+ font-size: 11.5px;
+ }
+
+ .tools-horizontal-bar-chart-loading,
+ .tools-horizontal-bar-chart-error,
+ .tools-horizontal-bar-chart-empty {
+ height: 165px;
+ }
+}
+
+@media (min-width: 429px) and (max-width: 480px) {
+ .tools-horizontal-bar-chart-card {
+ padding: 9px;
+ min-height: auto;
+ max-height: none;
+ }
+
+ .tools-horizontal-bar-chart-title {
+ font-size: 13.5px;
+ margin-bottom: 9px;
+ }
+
+ .tools-horizontal-bar-chart-filters {
+ padding: 7px 0;
+ margin-bottom: 9px;
+ }
+
+ .tools-horizontal-bar-chart-date-picker-group {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+ }
+
+ .tools-horizontal-bar-chart-date-picker {
+ min-width: 95px;
+ width: 100%;
+ }
+
+ .tools-horizontal-bar-chart-loading,
+ .tools-horizontal-bar-chart-error,
+ .tools-horizontal-bar-chart-empty {
+ height: 175px;
+ }
+}
+
+@media (min-width: 481px) and (max-width: 768px) {
+ .tools-horizontal-bar-chart-card {
+ padding: 10px;
+ min-height: auto;
+ max-height: none;
}
.tools-horizontal-bar-chart-title {
font-size: 14px;
- margin-bottom: 12px;
+ margin-bottom: 10px;
+ }
+
+ .tools-horizontal-bar-chart-filters {
+ padding: 8px 0;
+ margin-bottom: 10px;
}
.tools-horizontal-bar-chart-date-picker-group {
@@ -248,3 +366,17 @@
width: 100%;
}
}
+
+ .tools-horizontal-bar-chart-loading,
+ .tools-horizontal-bar-chart-error,
+ .tools-horizontal-bar-chart-empty {
+ height: 180px;
+ }
+
+/* Tablet adjustments */
+@media (min-width: 769px) and (max-width: 1024px) {
+ .tools-horizontal-bar-chart-card {
+ padding: 12px;
+ min-height: auto;
+ }
+}
\ No newline at end of file
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css
index 2e78b44dff..271d7f95ec 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css
+++ b/src/components/BMDashboard/WeeklyProjectSummary/Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart.module.css
@@ -1,62 +1,64 @@
-/* Light mode for multi select */
-:global(.customSelect__control) {
- background-color: #fff !important;
- color: #000 !important;
- border-color: #ccc;
- font-size: 1rem;
-}
+/* React-select styles are handled EXCLUSIVELY via inline styles in the component */
-:global(.customSelect__single-value) {
- font-size: 1rem;
- color: #000 !important;
-}
+/* No global CSS overrides - inline styles have higher specificity and are the proper way */
-.datepickerWrapper {
- display: flex;
- align-items: center;
- gap: 8px;
-}
+/* Removing any global overrides that might conflict */
-.datePickerContainer {
- flex: 0 0 auto;
- position: relative;
-}
+/* Date picker input - consistent sizing in all modes */
-.datePickerControl {
- display: block;
-}
-
-.datepickerWrapper :global(.btn) {
- flex: 0 0 auto;
+/* Base styles define size - dark mode only changes colors */
+:global(.datePickerInput),
+:global(.react-datepicker-wrapper input),
+:global(.react-datepicker-wrapper .datePickerInput) {
+ font-size: 13px !important;
+ padding: 8px 12px !important;
+ height: 38px !important;
+ line-height: 1.5 !important;
+ border-radius: 6px !important;
+ border-width: 1px !important;
+ border-style: solid !important;
+ box-sizing: border-box !important;
+ width: 100% !important;
+ min-width: 0 !important;
+ background-color: #fff !important;
+ color: #000 !important;
+ border-color: #ccc !important;
}
-:global(.customSelect__placeholder),
-:global(.customSelect__input-container) {
- font-size: 1rem;
+/* Date picker input - dark mode (ONLY colors change, sizes stay the same) */
+:global(.react-datepicker-wrapper.darkThemeDatePickerWrapper .datePickerInput),
+:global(.react-datepicker-wrapper.darkThemeDatePickerWrapper input),
+:global(.tools-availability-page.dark-mode .react-datepicker-wrapper input.datePickerInput),
+:global(.tools-availability-page.dark-mode .datePickerInput),
+:global(.tools-availability-page.dark-mode .react-datepicker-wrapper input),
+:global(.react-datepicker-wrapper.darkThemeDatePickerWrapper input[type="text"]),
+:global(.react-datepicker-wrapper.darkThemeDatePickerWrapper input[type="text"].datePickerInput) {
+ /* Only override colors - sizes inherit from base */
+ background-color: #2b3e59 !important;
+ color: #fff !important;
+ border-color: #666 !important;
}
-.datePickerInput {
- font-size: 1rem !important;
- padding: 6px 8px;
- border-radius: 4px;
+/* Light mode calendar styles */
+:global(.customCalendar) {
+ background-color: #fff !important;
+ color: #000 !important;
+ border: 1px solid #ccc !important;
}
-.datePickerInput:global(.darkTheme) {
- background-color: #2b3e59 !important;
- color: #fff !important;
- border: 1px solid #666 !important;
- font-size: 1rem !important;
+:global(.customCalendar .react-datepicker__header) {
+ background-color: #f0f0f0 !important;
+ color: #000 !important;
+ border-bottom: 1px solid #ccc !important;
}
-:global(.darkTheme .form-control .datePickerInput) {
- background-color: #2b3e59 !important;
- color: #fff !important;
- border: 1px solid #666 !important;
- font-size: 10px !important;
+:global(.customCalendar .react-datepicker__day) {
+ color: #000 !important;
}
:global(.customCalendar .react-datepicker__day--keyboard-selected) {
- background-color: #7d7e7f !important;
+ background-color: #5a5a5a !important;
+ color: #fff !important;
}
:global(.customCalendar .react-datepicker__navigation-icon::before) {
@@ -65,11 +67,22 @@
/* Hover day styling in dark theme */
:global(.customCalendar .react-datepicker__day:hover) {
- background-color: #3b5a81 !important;
+ background-color: #e0e0e0 !important;
+ color: #000 !important;
+ border-radius: 50%;
+}
+
+:global(.customCalendar .react-datepicker__day--selected) {
+ background-color: #06c !important;
color: #fff !important;
border-radius: 50%;
}
+:global(.customCalendar .react-datepicker__current-month),
+:global(.customCalendar .react-datepicker__day-name) {
+ color: #000 !important;
+}
+
/* Dark theme calendar container */
:global(.darkThemeCalendar) {
background-color: #2b3e59 !important;
@@ -77,6 +90,11 @@
border: 1px solid #6b6767 !important;
}
+:global(.darkThemeCalendar .react-datepicker__current-month),
+:global(.darkThemeCalendar .react-datepicker__day-name) {
+ color: #fff !important;
+}
+
/* Dark theme header */
:global(.darkThemeCalendar .react-datepicker__header) {
background-color: #1c2541 !important;
@@ -87,7 +105,7 @@
/* Selected day styling in dark theme */
:global(.darkThemeCalendar .react-datepicker__day--selected) {
- background-color: #0050a8 !important;
+ background-color: #1a5fa8 !important;
color: #fff !important;
border-radius: 50%;
}
@@ -103,65 +121,261 @@
border-radius: 50%;
}
-/* Dark mode for multi select */
-:global(.darkTheme .customSelect__control) {
- background-color: #fff!important;
- color: #000!important;
- border-color: #ccc!important;
- font-size: 1rem!important;
+/* React-select styling is handled ONLY via inline styles in JSX */
+
+/* No CSS overrides needed or wanted - inline styles are the proper method */
+
+/* Chart container - sized by explicit chart height, no overflow clipping */
+:global(.tools-horizontal-chart-container) {
+ width: 100%;
+ max-width: 100%;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ background-color: #fff !important; /* White background to show through transparent canvas */
+ overflow: visible; /* Allow chart labels and tooltips to render outside */
+}
+
+/* Dark mode for chart container */
+:global(.tools-availability-page.dark-mode .tools-horizontal-chart-container) {
+ background-color: #2c3344 !important;
+}
+
+/* Ensure Chart.js canvas wrapper has background */
+:global(.tools-horizontal-chart-container > div) {
+ background-color: #fff !important;
+}
+
+:global(.tools-availability-page.dark-mode .tools-horizontal-chart-container > div) {
+ background-color: #2c3344 !important;
+}
+
+/* Override min-height for tools-availability-page when used in card context */
+:global(.tools-availability-page) {
+ min-height: auto !important; /* Override 100vh from ToolsAvailabilityPage.css */
+ height: 100%; /* Fill parent card height */
+ padding: 8px; /* Remove excessive padding (24px) since card already has padding */
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ background-color: #fff !important; /* White background in light mode to prevent grey parent card background from showing through - !important to override any conflicting styles */
+ display: flex;
+ flex-direction: column;
+}
+
+/* Dark mode background for tools-availability-page */
+:global(.tools-availability-page.dark-mode) {
+ background-color: #2c3344 !important; /* Dark background in dark mode */
+}
+
+/* Chart title styling */
+:global(.tools-availability-page .tools-chart-title) {
+ flex-shrink: 0;
+ color: #000;
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ text-align: center;
+}
+
+/* Dark mode for chart title */
+:global(.tools-availability-page.dark-mode .tools-chart-title) {
+ color: #fff !important;
}
-:global(.darkTheme .customSelect__menu) {
- background-color: #2b3e59!important;
- border-color: #666!important;
- color: #fff!important;
+:global(.tools-availability-page .row) {
+ flex-shrink: 0;
}
-:global(.darkTheme .customSelect__option) {
- background-color: #2b3e59!important;
- color: #fff!important;
+/* Filters row - ensure consistent spacing */
+.filtersRow {
+ gap: 12px;
+ align-items: flex-end; /* Align filter inputs at the bottom */
}
-:global(.darkTheme .customSelect__option--is-focused) {
- background-color: #6c757d!important;
+.resetWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+ min-width: 0;
}
-:global(.darkTheme .customSelect__option--is-selected) {
- background-color: #007bff!important;
+.resetButton {
+ width: 100%;
+ height: 38px; /* Match other input heights */
+ font-size: 13px;
+ font-weight: 500;
}
-.darkSelectWrapper :global(.customSelect__control) {
- background-color: #1a1a1a !important;
- border-color: rgb(255 255 255 / 20%);
- color: #f5f7fa !important;
+/* Filter wrapper - symmetric and modular layout */
+.filterWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+ min-width: 0; /* Allow shrinking */
}
-.darkSelectWrapper :global(.customSelect__single-value),
-.darkSelectWrapper :global(.customSelect__input-container),
-.darkSelectWrapper :global(.customSelect__placeholder) {
- color: #f5f7fa !important;
+.datepickerWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+ max-width: 100%;
+ min-width: 0; /* Allow shrinking */
}
+/* Date picker input group - includes input and clear button */
+.datePickerInputGroup {
+ display: flex;
+ gap: 8px;
+ align-items: stretch;
+ width: 100%;
+}
+.datePickerInputGroup :global(.react-datepicker-wrapper) {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+}
+
+.datePickerInputGroup :global(.react-datepicker-wrapper input) {
+ flex: 1;
+ min-width: 0;
+}
-/* Filter controls layout for small screens */
-@media (width <= 768px) {
- :global(.datepickerWrapper) {
- display: flex;
- flex-direction: column;
- align-items: stretch;
+.datePickerInputGroup :global(button) {
+ flex-shrink: 0;
+ min-width: 38px;
+ height: 38px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+}
+
+/* Filter label styles - consistent across all filters */
+.filterLabel {
+ font-size: 13px;
+ font-weight: 600;
+ color: #000;
+ margin: 0;
+ line-height: 1.4;
+ display: block;
+ min-height: 19px; /* Ensure consistent height for alignment */
+}
+
+/* Dark mode for filter labels - white text */
+:global(.tools-availability-page.dark-mode) .filterLabel,
+:global(.tools-availability-page.dark-mode .filterLabel) {
+ color: #fff !important;
+}
+
+/* Filter controls layout for small screens - gradient scaling */
+@media (width <= 480px) {
+ :global(.tools-availability-page) {
+ padding: 4px;
}
- :global(.datepickerWrapper .btn) {
- margin-top: 8px;
+ :global(.tools-chart-title) {
+ font-size: 0.85rem;
+ margin-bottom: 0.6rem;
+ }
+
+ /* Bootstrap Row/Col fixes for mobile */
+ :global(.tools-availability-page .row) {
width: 100%;
+ max-width: 100%;
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ :global(.tools-availability-page .col),
+ :global(.tools-availability-page [class*='col-']) {
+ width: 100%;
+ max-width: 100%;
+ padding-left: 8px;
+ padding-right: 8px;
+ margin-bottom: 8px;
+ }
+
+ .datepickerWrapper {
+ gap: 5px;
}
- :global(.customSelect__control) {
- font-size: 1rem;
+ .filterWrapper {
+ gap: 5px;
+ }
+
+ .resetWrapper {
+ gap: 5px;
+ }
+
+ .filterLabel {
+ font-size: 11px;
+ min-height: 16px;
+ }
+
+ .resetButton {
+ height: 36px;
+ font-size: 11px;
+ }
+
+ :global(.tools-horizontal-chart-container) {
+ padding: 0;
+ }
+}
+
+@media (width >= 481px) and (width <= 768px) {
+ :global(.tools-availability-page) {
+ padding: 6px;
+ }
+
+ :global(.tools-chart-title) {
+ font-size: 0.9rem;
+ margin-bottom: 0.8rem;
+ }
+
+ /* Bootstrap Row/Col fixes for mobile */
+ :global(.tools-availability-page .row) {
+ width: 100%;
+ max-width: 100%;
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ :global(.tools-availability-page .col),
+ :global(.tools-availability-page [class*='col-']) {
+ width: 100%;
+ max-width: 100%;
+ padding-left: 8px;
+ padding-right: 8px;
+ margin-bottom: 8px;
+ }
+
+ .filterLabel {
+ font-size: 11.5px;
+ min-height: 17px;
+ }
+
+ .resetWrapper {
+ gap: 5px;
+ }
+
+ .resetButton {
+ height: 36px;
+ font-size: 11.5px;
+ }
+}
+
+ :global(.tools-horizontal-chart-container) {
+ padding: 0;
}
- :global(.customSelect__multi-value) {
- font-size: 1rem;
+/* Tablet adjustments */
+@media (width >= 769px) and (width <= 1024px) {
+ :global(.tools-availability-page) {
+ padding: 10px;
}
}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css
index 2a9ad25ff0..14e656fca8 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css
+++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css
@@ -225,6 +225,9 @@
max-width: 100%;
box-sizing: border-box;
overflow: hidden visible;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
.darkMode .weeklyProjectSummaryCard {
@@ -306,7 +309,7 @@
.wideCard {
width: 100%;
- min-height: 250px;
+ min-height: auto; /* Let content determine height instead of forcing 250px */
grid-column: span 2;
}
@@ -320,6 +323,7 @@
max-width: 100%;
min-height: 250px;
box-sizing: border-box;
+ min-height: auto; /* Let content determine height instead of forcing 250px */
}
.mapCard {
@@ -359,6 +363,7 @@
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
+ align-items: stretch; /* Make all cards in a row the same height */
border-top: 1px solid var(--card-shadow);
animation: fade-in 0.3s ease-in-out;
background: var(--section-bg);
@@ -686,6 +691,27 @@
/* Medium Screens */
@media (width <= 1024px) {
+/* Large screens - Tools section layout optimization */
+@media (max-width: 1366px) {
+ /* Tools and Equipment Tracking - Switch to 2 column layout earlier for better chart visibility */
+ .weeklyProjectSummaryDashboardCategoryContent {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+ padding: 12px;
+ }
+
+ /* Wide card spans 2 columns, normal cards span 1 column each */
+ .weeklyProjectSummaryDashboardCategoryContent .wideCard {
+ grid-column: span 2;
+ }
+
+ .weeklyProjectSummaryDashboardCategoryContent .normalCard {
+ grid-column: span 1;
+ }
+}
+
+/* Medium Screens - Wrap Items */
+@media (max-width: 1024px) {
.weeklySummaryHeaderContainer {
flex-direction: column;
align-items: center;
@@ -700,10 +726,86 @@
.weeklyProjectSummaryDashboardGrid {
grid-template-columns: repeat(2, 1fr);
}
+
+ /* Tools and Equipment Tracking - 2 column layout on tablet */
+ .weeklyProjectSummaryDashboardCategoryContent {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+ padding: 12px;
+ }
+
+ /* Cards span full width on tablet */
+ .weeklyProjectSummaryDashboardCategoryContent .wideCard,
+ .weeklyProjectSummaryDashboardCategoryContent .normalCard {
+ grid-column: span 2;
+ }
}
/* Small Screens */
@media (width <= 768px) {
+/* Extra Small Screens - Small phones */
+@media (max-width: 375px) {
+ .weeklyProjectSummaryDashboardCategoryContent {
+ padding: 8px;
+ gap: 8px;
+ }
+
+ .weeklyProjectSummaryCard {
+ padding: 6px;
+ }
+
+ /* Ensure charts are fully visible on small mobile - prevent clipping */
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-chart-container,
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-bar-chart-content,
+ .weeklyProjectSummaryDashboardCategoryContent .tool-donut-wrapper {
+ width: 100%;
+ max-width: 100%;
+ overflow: hidden;
+ }
+}
+
+/* Small Screens - Medium phones */
+@media (min-width: 376px) and (max-width: 428px) {
+ .weeklyProjectSummaryDashboardCategoryContent {
+ padding: 10px;
+ gap: 10px;
+ }
+
+ .weeklyProjectSummaryCard {
+ padding: 7px;
+ }
+
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-chart-container,
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-bar-chart-content,
+ .weeklyProjectSummaryDashboardCategoryContent .tool-donut-wrapper {
+ width: 100%;
+ max-width: 100%;
+ overflow: hidden;
+ }
+}
+
+/* Small Screens - Large phones and tablets */
+@media (min-width: 429px) and (max-width: 768px) {
+ .weeklyProjectSummaryDashboardCategoryContent {
+ padding: 12px;
+ gap: 12px;
+ }
+
+ .weeklyProjectSummaryCard {
+ padding: 8px;
+ }
+
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-chart-container,
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-bar-chart-content,
+ .weeklyProjectSummaryDashboardCategoryContent .tool-donut-wrapper {
+ width: 100%;
+ max-width: 100%;
+ overflow: hidden;
+ }
+}
+
+/* Small Screens - Make Dropdowns Vertical */
+@media (max-width: 768px) {
.weeklySummaryHeaderContainer {
flex-direction: column;
align-items: center;
@@ -785,6 +887,27 @@
.financialsTrackingGrid {
grid-template-columns: 1fr;
}
+
+ /* Ensure charts are fully visible on mobile - prevent clipping */
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-chart-container,
+ .weeklyProjectSummaryDashboardCategoryContent .tools-horizontal-bar-chart-content,
+ .weeklyProjectSummaryDashboardCategoryContent .tool-donut-wrapper {
+ width: 100%;
+ max-width: 100%;
+ overflow: visible;
+ }
+}
+
+/* Extra small screens */
+@media (max-width: 576px) {
+ .weeklyProjectSummaryDashboardCategoryContent {
+ padding: 8px;
+ gap: 10px;
+ }
+
+ .weeklyProjectSummaryCard {
+ padding: 6px;
+ }
}
/* Extra Small Screens */
@@ -884,4 +1007,4 @@
.weeklySummaryHeaderControls select {
display: none;
}
-}
+}
\ No newline at end of file
diff --git a/src/utils/chartResponsiveUtils.js b/src/utils/chartResponsiveUtils.js
new file mode 100644
index 0000000000..3fc0718217
--- /dev/null
+++ b/src/utils/chartResponsiveUtils.js
@@ -0,0 +1,64 @@
+/**
+ * Shared responsive breakpoint utilities for BMDashboard charts.
+ * Uses consistent breakpoints (375, 428, 480, 768, 1024) to avoid duplication
+ * between ToolsHorizontalBarChart and ToolsStoppageHorizontalBarChart.
+ */
+
+const BREAKPOINTS = [375, 428, 480, 768, 1024];
+
+/**
+ * Resolve a value for current window width from an array of values per breakpoint.
+ * @param {number} windowWidth - Current window width
+ * @param {number[]} values - [at375, at428, at480, at768, at1024, default]
+ * @returns {number}
+ */
+export function getValueForBreakpoints(windowWidth, values) {
+ for (let i = 0; i < BREAKPOINTS.length; i++) {
+ if (windowWidth <= BREAKPOINTS[i]) return values[i];
+ }
+ return values.at(-1);
+}
+
+/** Chart height in px: 180 → 300 from small phone to desktop */
+export function getChartHeight(windowWidth) {
+ return getValueForBreakpoints(windowWidth, [180, 200, 220, 240, 280, 300]);
+}
+
+/** Recharts bar chart margins: { top, right, left, bottom } */
+export function getChartMargins(windowWidth) {
+ const tops = getValueForBreakpoints(windowWidth, [3, 4, 4, 5, 8, 10]);
+ const rights = getValueForBreakpoints(windowWidth, [3, 4, 4, 5, 15, 30]);
+ const lefts = getValueForBreakpoints(windowWidth, [12, 13, 14, 15, 25, 40]);
+ const bottoms = getValueForBreakpoints(windowWidth, [3, 4, 4, 5, 8, 10]);
+ return { top: tops, right: rights, left: lefts, bottom: bottoms };
+}
+
+/** Y-axis width for Recharts */
+export function getYAxisWidth(windowWidth) {
+ return getValueForBreakpoints(windowWidth, [18, 19, 19.5, 20, 28, 35]);
+}
+
+/** Font size for axis/tick labels (shared by both charts) */
+export function getChartFontSize(windowWidth) {
+ return getValueForBreakpoints(windowWidth, [8, 9, 9.5, 10, 11, 12]);
+}
+
+/** Chart.js maxBarThickness for ToolsStoppageHorizontalBarChart */
+export function getMaxBarThickness(windowWidth) {
+ return getValueForBreakpoints(windowWidth, [15, 16, 18, 20, 22, 25]);
+}
+
+/** Chart.js categoryPercentage for ToolsStoppageHorizontalBarChart */
+export function getCategoryPercentage(windowWidth) {
+ return getValueForBreakpoints(windowWidth, [0.45, 0.47, 0.48, 0.5, 0.55, 0.6]);
+}
+
+/** Chart.js barPercentage for ToolsStoppageHorizontalBarChart */
+export function getBarPercentage(windowWidth) {
+ return getValueForBreakpoints(windowWidth, [0.8, 0.82, 0.84, 0.85, 0.87, 0.9]);
+}
+
+/** Chart.js title font size for ToolsStoppageHorizontalBarChart */
+export function getChartTitleFontSize(windowWidth) {
+ return getValueForBreakpoints(windowWidth, [9, 10, 10.5, 11, 12, 14]);
+}
diff --git a/src/utils/reactSelectUtils.js b/src/utils/reactSelectUtils.js
new file mode 100644
index 0000000000..19cd0cf70c
--- /dev/null
+++ b/src/utils/reactSelectUtils.js
@@ -0,0 +1,109 @@
+/**
+ * Utility functions for react-select styling to reduce code duplication
+ * and cognitive complexity
+ */
+
+/**
+ * Get background color for react-select option based on state and theme
+ * @param {Object} state - react-select option state (isSelected, isFocused, etc.)
+ * @param {boolean} isDarkMode - Whether dark mode is enabled
+ * @returns {string} Background color hex code
+ */
+export const getOptionBackgroundColor = (state, isDarkMode) => {
+ if (state.isSelected) {
+ return isDarkMode ? '#e8a71c' : '#0d55b3';
+ }
+ if (state.isFocused) {
+ return isDarkMode ? '#3a506b' : '#f0f0f0';
+ }
+ return isDarkMode ? '#253342' : '#fff';
+};
+
+/**
+ * Get text color for react-select option based on state and theme
+ * @param {Object} state - react-select option state (isSelected, isFocused, etc.)
+ * @param {boolean} isDarkMode - Whether dark mode is enabled
+ * @returns {string} Text color hex code
+ */
+export const getOptionColor = (state, isDarkMode) => {
+ if (state.isSelected) {
+ return isDarkMode ? '#000' : '#fff';
+ }
+ return isDarkMode ? '#ffffff' : '#000';
+};
+
+/**
+ * Get standardized react-select styles matching paid-labor-cost pattern
+ * This reduces code duplication across components
+ * @param {boolean} isDarkMode - Whether dark mode is enabled
+ * @returns {Object} react-select styles object
+ */
+export const getStandardSelectStyles = isDarkMode => ({
+ control: base => ({
+ ...base,
+ minHeight: '38px',
+ fontSize: '12px',
+ backgroundColor: isDarkMode ? '#253342' : '#fff',
+ borderColor: isDarkMode ? '#2d4059' : '#ccc',
+ color: isDarkMode ? '#ffffff' : '#000',
+ boxShadow: 'none',
+ borderRadius: '6px',
+ '&:hover': {
+ borderColor: isDarkMode ? '#2d4059' : '#999',
+ },
+ }),
+ valueContainer: base => ({
+ ...base,
+ padding: '2px 8px',
+ color: isDarkMode ? '#ffffff' : '#000',
+ }),
+ input: base => ({
+ ...base,
+ margin: '0px',
+ padding: '0px',
+ color: isDarkMode ? '#ffffff' : '#000',
+ }),
+ indicatorsContainer: base => ({
+ ...base,
+ padding: '0 4px',
+ }),
+ menu: base => ({
+ ...base,
+ backgroundColor: isDarkMode ? '#253342' : '#fff',
+ fontSize: '12px',
+ }),
+ option: (base, state) => ({
+ ...base,
+ backgroundColor: getOptionBackgroundColor(state, isDarkMode),
+ color: getOptionColor(state, isDarkMode),
+ cursor: 'pointer',
+ padding: '8px 12px',
+ fontSize: '12px',
+ ':active': {
+ backgroundColor: isDarkMode ? '#3a506b' : '#e0e0e0',
+ },
+ }),
+ singleValue: base => ({
+ ...base,
+ color: isDarkMode ? '#ffffff' : '#000',
+ fontSize: '12px',
+ }),
+ placeholder: base => ({
+ ...base,
+ color: isDarkMode ? '#aaaaaa' : '#666',
+ fontSize: '12px',
+ }),
+ indicatorSeparator: base => ({
+ ...base,
+ backgroundColor: isDarkMode ? '#2d4059' : '#ccc',
+ }),
+ dropdownIndicator: base => ({
+ ...base,
+ color: isDarkMode ? '#ffffff' : '#999',
+ padding: '4px',
+ ':hover': {
+ color: isDarkMode ? '#ffffff' : '#666',
+ },
+ }),
+});
+
diff --git a/yarn.lock b/yarn.lock
index 05f438584b..2824898d6d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7524,6 +7524,15 @@ fast-png@^6.2.0:
iobuffer "^5.3.2"
pako "^2.1.0"
+fast-png@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fast-png/-/fast-png-7.0.1.tgz#db8de3aa5c79f9a450d273f171ca5c9e28240ded"
+ integrity sha512-aD5BELuxRrAPlRhb9V/z1PVMFJy3cUXqIvoxM3IQ+7Rku+T4cbXxWclZ47f1XwhViEl4n30TAN8JmvTJKKc2Dw==
+ dependencies:
+ "@types/pako" "^2.0.3"
+ iobuffer "^6.0.0"
+ pako "^2.1.0"
+
fast-string-truncated-width@^3.0.2:
version "3.0.3"
resolved "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz"
@@ -8534,6 +8543,11 @@ iobuffer@^5.3.2:
resolved "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz"
integrity sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==
+iobuffer@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/iobuffer/-/iobuffer-6.0.1.tgz#e550d671384362d541b5a23e1a81ed46ecab36d8"
+ integrity sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==
+
is-any-array@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-any-array/-/is-any-array-3.0.0.tgz"