diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index 7140444799..128f4dbf31 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -47,7 +47,7 @@ const projectStatusButtonData = [ ['Completed Projects', 'completedProjects', '#F3FCFF', '#C1EFFB', '#328D1B'], ['Delayed Projects', 'delayedProjects', '#FFE9FA', '#FECFF3', '#C82F2F'], ['Active Projects', 'activeProjects', '#E8E8FF', '#CBCBFE', '#328D1B'], - ['Avg Project Duration', 'avgProjectDuration', '#FFF6EE', '#FFD8A5', '#FFD8A5'], + ['Avg Completed Project Duration (hours)', 'avgProjectDuration', '#FFF6EE', '#FFD8A5', '#FFD8A5'], ['Total Material Cost', 'totalMaterialCost', '#FFF3F3', '#FBC1C2', '#328D1B'], ['Total Material Used', 'totalMaterialUsed', '#DAC8FF', '#B28ECC', '#328D1B'], ['Total Labor Hours Invested', 'totalLaborHoursInvested', '#E5C1FC', '#F6E1FB', '#328D1B'], @@ -325,12 +325,18 @@ function WeeklyProjectSummary() { {projectStatusCards.map(button => { const unavailableTooltipId = `project-status-unavailable-${button.metricKey}`; return ( -
{button.title}
+ {button.metricKey === 'avgProjectDuration' && ( +
+ Start to completion for projects completed in the selected period. +
+ )}
)} -
+ ); })}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css index 7d234634a8..6f8fc34d79 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css @@ -610,7 +610,9 @@ /* Fill the responsive grid track instead of capping cards at a fixed desktop width. */ max-width: none; min-width: 0; - height: 190px; + min-height: 190px; + height: auto; + align-self: stretch; text-align: center; box-shadow: 0 4px 8px rgb(0 0 0 / 15%); padding: 20px; @@ -627,6 +629,13 @@ transition: color 0.3s ease; } +.projectDurationDescription { + margin-top: 4px; + color: #333; + font-size: 12px; + line-height: 1.4; +} + .weeklyStatusValue { color: #000; font-size: 40px; @@ -686,6 +695,7 @@ } /* Keep all Project Status text readable on the preserved pastel status cards in dark mode. */ +.darkMode .statusCard .projectDurationDescription, .darkMode .statusCard .weeklyCardTitle, .darkMode .statusCard .weeklyStatusValue { color: #000 !important; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.projectStatus.test.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.projectStatus.test.jsx index 0a1e385653..0aaa629711 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.projectStatus.test.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.projectStatus.test.jsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; import WeeklyProjectSummary from './WeeklyProjectSummary'; @@ -163,6 +163,75 @@ describe('WeeklyProjectSummary Project Status request state', () => { expect(screen.queryByRole('alert')).not.toBeInTheDocument(); }); + it.each([false, true])('renders Active Projects only once (dark mode: %s)', darkMode => { + const state = createState(); + state.theme.darkMode = darkMode; + state.weeklyProjectSummary.projectStatusData = { + current: { metrics: { activeProjects: { value: 265, comparisonType: 'CURRENT_ONLY' } } }, + }; + renderWithState(state); + + expect(screen.getAllByText('Active Projects')).toHaveLength(1); + expect(screen.getAllByText('265')).toHaveLength(1); + }); + + it.each([0, 17, 1008])( + 'defines the duration metric and preserves the API hour value %s', + value => { + const state = createState(); + state.weeklyProjectSummary.projectStatusData = { + current: { + metrics: { + avgProjectDuration: { + value, + unit: 'hrs', + comparisonType: 'PERIOD_COMPARABLE', + percentageChange: 0.13, + }, + }, + }, + }; + renderWithState(state); + + const durationCard = screen.getByRole('region', { + name: 'Avg Completed Project Duration (hours)', + }); + expect(within(durationCard).getByText(`${value} hrs`)).toBeInTheDocument(); + expect( + within(durationCard).getByText( + 'Start to completion for projects completed in the selected period.', + ), + ).toBeInTheDocument(); + expect(within(durationCard).getByText('+13% month over month')).toBeInTheDocument(); + }, + ); + + it('keeps missing duration unavailable instead of inventing 17 hours', () => { + const state = createState(); + state.weeklyProjectSummary.projectStatusData = { + current: { + metrics: { + avgProjectDuration: { + value: null, + unit: 'hrs', + comparisonType: 'UNAVAILABLE', + unavailableReason: 'No completed projects in this period.', + }, + }, + }, + }; + renderWithState(state); + + const durationCard = screen.getByRole('region', { + name: 'Avg Completed Project Duration (hours)', + }); + expect(within(durationCard).getByText('N/A')).toBeInTheDocument(); + expect( + within(durationCard).getByRole('button', { name: 'No completed projects in this period.' }), + ).toBeInTheDocument(); + expect(screen.queryByText('17 hrs')).not.toBeInTheDocument(); + }); + it('shows loading and suppresses stale comparison text when filters change before useEffect dispatches', () => { renderWithState( createState({