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({