diff --git a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx
index 15929b0329..2edf5930b6 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import Select from 'react-select';
import { useSelector } from 'react-redux';
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts';
@@ -9,12 +9,13 @@ const COLORS = ['#2a647c', '#2e8ea3', '#ffab91', '#ffccbb', '#bbbbbb', '#f9f3e3'
const CustomTooltip = ({ active, payload, total, darkMode }) => {
if (active && payload && payload.length) {
const { name, value } = payload[0];
- const percent = ((value / total) * 100).toFixed(1);
+ const percent = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
+
return (
{
);
}
+
return null;
};
@@ -33,37 +35,152 @@ export default function DistributionLaborHours() {
const [originalData, setOriginalData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
- const [dateRange, setDateRange] = useState({ from: '', to: '' });
+
+ // Values currently selected by the user.
+ const [dateRange, setDateRange] = useState({
+ from: '',
+ to: '',
+ });
const [projectFilter, setProjectFilter] = useState('');
const [memberFilter, setMemberFilter] = useState('');
+ // Gives the user visible confirmation that Submit worked.
+ const [filtersApplied, setFiltersApplied] = useState(false);
+
useEffect(() => {
const fetchData = async () => {
+ /*
+ * Mock data for now.
+ *
+ * project, member, and date have been added so that the filter controls
+ * and Submit button can actually demonstrate filtering.
+ *
+ * When backend data is connected, replace this array with the API result.
+ */
const mockData = [
- { name: 'Stud Wall Construction', value: 25.9 },
- { name: 'Foundation Concreting', value: 18.5 },
- { name: 'Task A', value: 22.2 },
- { name: 'Task B', value: 18.5 },
- { name: 'Task C', value: 14.8 },
- { name: 'Electrical', value: 12 },
- { name: 'Plumbing', value: 8 },
- { name: 'Welding', value: 6 },
+ {
+ name: 'Stud Wall Construction',
+ value: 25.9,
+ project: 'Project A',
+ member: 'Member 1',
+ date: '2026-08-05',
+ },
+ {
+ name: 'Foundation Concreting',
+ value: 18.5,
+ project: 'Project A',
+ member: 'Member 2',
+ date: '2026-08-09',
+ },
+ {
+ name: 'Task A',
+ value: 22.2,
+ project: 'Project B',
+ member: 'Member 1',
+ date: '2026-08-12',
+ },
+ {
+ name: 'Task B',
+ value: 18.5,
+ project: 'Project B',
+ member: 'Member 2',
+ date: '2026-08-16',
+ },
+ {
+ name: 'Task C',
+ value: 14.8,
+ project: 'Project A',
+ member: 'Member 1',
+ date: '2026-08-20',
+ },
+ {
+ name: 'Electrical',
+ value: 12,
+ project: 'Project A',
+ member: 'Member 2',
+ date: '2026-08-22',
+ },
+ {
+ name: 'Plumbing',
+ value: 8,
+ project: 'Project B',
+ member: 'Member 1',
+ date: '2026-08-24',
+ },
+ {
+ name: 'Welding',
+ value: 6,
+ project: 'Project B',
+ member: 'Member 2',
+ date: '2026-08-27',
+ },
];
+
setOriginalData(mockData);
};
+
fetchData();
}, []);
- useEffect(() => {
- const top5 = originalData.slice(0, 5);
- const othersTotal = originalData.slice(5).reduce((sum, item) => sum + item.value, 0);
+ /*
+ * Format filtered rows as:
+ * top 5 tasks + combined "Others" row.
+ */
+ const formatChartData = data => {
+ const sortedData = [...data].sort((a, b) => b.value - a.value);
+
+ const top5 = sortedData.slice(0, 5).map(({ name, value }) => ({
+ name,
+ value,
+ }));
+
+ const othersTotal = sortedData.slice(5).reduce((sum, item) => sum + item.value, 0);
+
if (othersTotal > 0) {
- top5.push({ name: 'Others', value: othersTotal });
+ top5.push({
+ name: 'Others',
+ value: Number(othersTotal.toFixed(1)),
+ });
+ }
+
+ return top5;
+ };
+
+ /*
+ * Display all data when the component initially loads.
+ */
+ useEffect(() => {
+ if (originalData.length > 0) {
+ setFilteredData(formatChartData(originalData));
+ }
+ }, [originalData]);
+
+ const handleSubmit = () => {
+ let result = [...originalData];
+
+ if (dateRange.from) {
+ result = result.filter(item => item.date >= dateRange.from);
+ }
+
+ if (dateRange.to) {
+ result = result.filter(item => item.date <= dateRange.to);
+ }
+
+ if (projectFilter) {
+ result = result.filter(item => item.project === projectFilter);
+ }
+
+ if (memberFilter) {
+ result = result.filter(item => item.member === memberFilter);
}
- setFilteredData(top5);
- }, [originalData, dateRange, projectFilter, memberFilter]);
- const totalHours = filteredData.reduce((sum, item) => sum + item.value, 0);
+ setFilteredData(formatChartData(result));
+ setFiltersApplied(true);
+ };
+
+ const totalHours = useMemo(() => filteredData.reduce((sum, item) => sum + item.value, 0), [
+ filteredData,
+ ]);
const projectOptions = [
{ value: '', label: 'ALL' },
@@ -77,105 +194,180 @@ export default function DistributionLaborHours() {
{ value: 'Member 2', label: 'Member 2' },
];
+ const selectedProject =
+ projectOptions.find(option => option.value === projectFilter) || projectOptions[0];
+
+ const selectedMember =
+ memberOptions.find(option => option.value === memberFilter) || memberOptions[0];
+
+ /*
+ * Rendering the React Select menu in document.body prevents the menu
+ * from being clipped or closed unexpectedly by dashboard containers.
+ */
+ const menuPortalTarget = typeof document !== 'undefined' ? document.body : null;
+
+ const selectStyles = {
+ menuPortal: base => ({
+ ...base,
+ zIndex: 9999,
+ }),
+ menu: base => ({
+ ...base,
+ zIndex: 9999,
+ }),
+ };
+
return (
Distribution of Labor Hours
- {/* Filters */}
-
- {/* Chart + Legend */}
-
- {filteredData.map((entry, index) => (
-
-
-
- {entry.name}: {entry.value} hrs
-
+ {filteredData.length > 0 ? (
+ <>
+
+ {filteredData.map((entry, index) => (
+
+
+
+
+ {entry.name}: {entry.value} hrs
+
+
+ ))}
- ))}
-
-
-
-
- (
-
+
+
+ (
+
+ {totalHours > 0 ? `${((value / totalHours) * 100).toFixed(1)}%` : '0.0%'}
+
+ )}
>
- {`${((value / totalHours) * 100).toFixed(1)}%`}
-
- )}
- >
- {filteredData.map((entry, index) => (
- |
- ))}
-
- } />
-
-
-
+ {filteredData.map((entry, index) => (
+
|
+ ))}
+
+
+
} />
+
+
+
+ >
+ ) : (
+
No labor-hour data found for the selected filters.
+ )}
);
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css
index e59d56e8a9..165f919641 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css
+++ b/src/components/BMDashboard/WeeklyProjectSummary/DistributionLaborHours/DistributionLaborHours.module.css
@@ -1,4 +1,5 @@
-/* stylelint-disable */
+/* stylelint-disable */
+
.container {
border-radius: 8px;
padding: 12px;
@@ -7,7 +8,9 @@
height: 100%;
display: flex;
flex-direction: column;
- transition: background-color 0.3s ease, color 0.3s ease;
+ transition:
+ background-color 0.3s ease,
+ color 0.3s ease;
}
.title {
@@ -25,40 +28,74 @@
color: #2563eb !important;
}
+/* =========================
+ FILTERS
+ ========================= */
+
.filters {
display: flex;
flex-wrap: wrap;
justify-content: center;
+ align-items: flex-end;
gap: 12px;
margin-bottom: 16px;
}
-.filters label {
+.filterGroup {
display: flex;
flex-direction: column;
+ gap: 5px;
+ min-width: 110px;
+}
+
+.filterGroup label {
+ display: block;
font-size: 0.9rem;
+ font-weight: 600;
+ margin: 0;
}
-/* Dark mode: make filter labels readable */
-:global(.dark) .filters label,
-:global(.dark-mode) .filters label,
-:global([data-theme='dark']) .filters label {
+/* Dark mode filter labels */
+:global(.dark) .filterGroup label,
+:global(.dark-mode) .filterGroup label,
+:global(.bm-dashboard-dark) .filterGroup label,
+:global([data-theme='dark']) .filterGroup label {
color: #fff;
}
-/* Scope to date inputs only — react-select renders its own inside
- .filters, and this box styling must not apply to it. */
-.filters input[type='date'] {
- height: auto !important;
+/* Date inputs */
+.filterGroup input[type='date'] {
+ min-height: 38px;
border-radius: 6px;
box-sizing: border-box;
font-size: 0.95rem;
line-height: 1.2;
+ padding: 7px 9px;
+ border: 1px solid #d1d5db;
}
-/* Give the dropdowns enough width and keep option text on one line */
+/* Dark mode date input */
+:global(.dark) .filterGroup input[type='date'],
+:global(.dark-mode) .filterGroup input[type='date'],
+:global(.bm-dashboard-dark) .filterGroup input[type='date'],
+:global([data-theme='dark']) .filterGroup input[type='date'] {
+ background-color: #1f2937 !important;
+ color: #f9fafb !important;
+ border: 1px solid #374151 !important;
+ color-scheme: dark;
+}
+
+/* =========================
+ REACT SELECT
+ ========================= */
+
:global(.react-select-container) {
- min-width: 100px;
+ min-width: 110px;
+}
+
+:global(.react-select__control) {
+ min-height: 38px !important;
+ cursor: pointer !important;
}
:global(.react-select__option),
@@ -67,81 +104,159 @@
font-weight: normal !important;
}
+:global(.react-select__menu) {
+ z-index: 9999 !important;
+}
+
+:global(.react-select__menu-portal) {
+ z-index: 9999 !important;
+}
+
+/* Dark mode select control */
:global(.dark) :global(.react-select__control),
:global(.dark-mode) :global(.react-select__control),
+:global(.bm-dashboard-dark) :global(.react-select__control),
:global([data-theme='dark']) :global(.react-select__control) {
background-color: #1f2937 !important;
border-color: #374151 !important;
}
+/* Selected text */
:global(.dark) :global(.react-select__single-value),
:global(.dark-mode) :global(.react-select__single-value),
-:global([data-theme='dark']) :global(.react-select__single-value),
+:global(.bm-dashboard-dark) :global(.react-select__single-value),
+:global([data-theme='dark']) :global(.react-select__single-value) {
+ color: #f9fafb !important;
+}
+
+/* React Select internal input */
:global(.dark) :global(.react-select__input),
:global(.dark-mode) :global(.react-select__input),
+:global(.bm-dashboard-dark) :global(.react-select__input),
:global([data-theme='dark']) :global(.react-select__input) {
color: #f9fafb !important;
}
+/* Placeholder */
:global(.dark) :global(.react-select__placeholder),
:global(.dark-mode) :global(.react-select__placeholder),
+:global(.bm-dashboard-dark) :global(.react-select__placeholder),
:global([data-theme='dark']) :global(.react-select__placeholder) {
color: #9ca3af !important;
}
-/* Dim the vertical separator between the value and the dropdown arrow */
+/* Separator */
:global(.react-select__indicator-separator) {
background-color: #ccc !important;
}
-/* Dark mode: use the panel color for the separator */
:global(.dark) :global(.react-select__indicator-separator),
:global(.dark-mode) :global(.react-select__indicator-separator),
+:global(.bm-dashboard-dark) :global(.react-select__indicator-separator),
:global([data-theme='dark']) :global(.react-select__indicator-separator) {
- background-color: #2d4059 !important;
+ background-color: #4b5563 !important;
}
-/* Make the dropdown arrow a light gray (not bright white) in both modes */
+/* Arrow */
:global(.react-select__dropdown-indicator) {
color: #ccc !important;
}
+/* Dark dropdown menu */
:global(.dark) :global(.react-select__menu),
:global(.dark-mode) :global(.react-select__menu),
+:global(.bm-dashboard-dark) :global(.react-select__menu),
:global([data-theme='dark']) :global(.react-select__menu) {
background-color: #1f2937 !important;
border: 1px solid #374151 !important;
- z-index: 10;
}
+/* Dark dropdown options */
:global(.dark) :global(.react-select__option),
:global(.dark-mode) :global(.react-select__option),
+:global(.bm-dashboard-dark) :global(.react-select__option),
:global([data-theme='dark']) :global(.react-select__option) {
background-color: #1f2937 !important;
color: #f9fafb !important;
+ cursor: pointer;
}
+/* Focused option */
:global(.dark) :global(.react-select__option--is-focused),
:global(.dark-mode) :global(.react-select__option--is-focused),
+:global(.bm-dashboard-dark) :global(.react-select__option--is-focused),
:global([data-theme='dark']) :global(.react-select__option--is-focused) {
background-color: #374151 !important;
- color: #ffffff !important;
+ color: #fff !important;
}
-/* Dark mode: style date inputs only (not react-select's internal input) */
-:global(.dark) .filters input[type='date'],
-:global(.dark-mode) .filters input[type='date'],
-:global([data-theme='dark']) .filters input[type='date'] {
- background-color: #1f2937 !important;
- color: #f9fafb !important;
- border: 1px solid #374151 !important;
+
+/* Selected option */
+:global(.dark) :global(.react-select__option--is-selected),
+:global(.dark-mode) :global(.react-select__option--is-selected),
+:global(.bm-dashboard-dark) :global(.react-select__option--is-selected),
+:global([data-theme='dark']) :global(.react-select__option--is-selected) {
+ background-color: #2563eb !important;
+ color: #fff !important;
}
-:global(.dark) .filters input[type='date'],
-:global(.dark-mode) .filters input[type='date'],
-:global([data-theme='dark']) .filters input[type='date'] {
- color-scheme: dark;
+/* =========================
+ SUBMIT BUTTON
+ ========================= */
+
+.buttonContainer {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ margin-top: 5px;
+ min-height: 38px;
+}
+
+.button {
+ background-color: #2563eb;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ padding: 8px 16px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+}
+
+.button:hover {
+ background-color: #1d4ed8;
+}
+
+.button:focus-visible {
+ outline: 2px solid #93c5fd;
+ outline-offset: 2px;
+}
+
+:global(.dark) .button,
+:global(.dark-mode) .button,
+:global(.bm-dashboard-dark) .button,
+:global([data-theme='dark']) .button {
+ background-color: #2563eb;
+ color: #fff;
+}
+
+.appliedMessage {
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: #166534;
+}
+
+:global(.dark) .appliedMessage,
+:global(.dark-mode) .appliedMessage,
+:global(.bm-dashboard-dark) .appliedMessage,
+:global([data-theme='dark']) .appliedMessage {
+ color: #86efac;
}
+/* =========================
+ CHART
+ ========================= */
+
.chartWrapper {
display: flex;
justify-content: space-between;
@@ -163,77 +278,79 @@
font-size: 0.85rem;
}
+.legendText {
+ line-height: 1.25;
+}
+
.colorBox {
width: 12px;
height: 12px;
margin-right: 8px;
border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.pieChartContainer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
}
+/* =========================
+ TOOLTIP
+ ========================= */
+
.tooltip {
background-color: #f3f4f6;
- color: #111827; /* darker percentage text in light mode */
+ color: #111827;
padding: 8px;
border-radius: 6px;
}
-/* Dark mode: ensure tooltip text is white on hover */
+.tooltip p {
+ margin: 2px 0;
+}
+
:global(.dark) .tooltip,
:global(.dark-mode) .tooltip,
+:global(.bm-dashboard-dark) .tooltip,
:global([data-theme='dark']) .tooltip {
color: #fff;
}
-:global(.dark) .tooltip:hover,
-:global(.dark-mode) .tooltip:hover,
-:global([data-theme='dark']) .tooltip:hover {
- color: #fff;
-}
-
-/* Dark mode: force ALL tooltip text to white */
:global(.dark) .tooltip *,
:global(.dark-mode) .tooltip *,
+:global(.bm-dashboard-dark) .tooltip *,
:global([data-theme='dark']) .tooltip * {
color: #fff !important;
}
-.buttonContainer {
- width: 100%;
+/* =========================
+ NO DATA
+ ========================= */
+
+.noData {
+ width: 100%;
+ min-height: 250px;
display: flex;
+ align-items: center;
justify-content: center;
- margin-top: 5px;
-}
-
-/* Submit button – light & dark mode safe */
-.button {
- background-color: #2563eb; /* blue */
- color: #fff; /* white text */
- border: none;
- border-radius: 6px;
- padding: 8px 16px;
- cursor: pointer;
- transition: background-color 0.3s ease;
-}
-
-/* Hover */
-.button:hover {
- background-color: #1d4ed8;
+ text-align: center;
+ font-size: 0.95rem;
+ color: #4b5563;
}
-/* Dark mode – keep same button color (no brightness jump) */
-:global(.dark) .button,
-:global(.dark-mode) .button,
-:global([data-theme='dark']) .button {
- background-color: #2563eb;
- color: #fff;
+:global(.dark) .noData,
+:global(.dark-mode) .noData,
+:global(.bm-dashboard-dark) .noData,
+:global([data-theme='dark']) .noData {
+ color: #d1d5db;
}
-.pieChartContainer {
- display: flex;
- justify-content: center;
- align-items: center;
- width: 100%;
-}
+/* =========================
+ RESPONSIVE
+ ========================= */
@media (width <= 768px) {
.chartWrapper {
@@ -243,7 +360,7 @@
.legend {
margin-bottom: 16px;
- align-items: center;
+ align-items: flex-start;
}
.filters {
@@ -252,7 +369,16 @@
gap: 10px;
}
- .filters label {
+ .filterGroup {
width: 100%;
+ max-width: 280px;
+ }
+
+ :global(.react-select-container) {
+ width: 100%;
+ }
+
+ .buttonContainer {
+ max-width: 280px;
}
}
\ No newline at end of file