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