From f0fdb2ac4e8bb822fe455cb1607aaea7c6c536f4 Mon Sep 17 00:00:00 2001
From: Michael Fisher <mfisher31@gmail.com>
Date: Mon, 15 Jun 2026 11:33:04 -0400
Subject: [PATCH] ui: consistently round durations and time stamps

---
 src/features/clock/TimesheetPage.tsx      | 15 ++++++++----
 src/features/clock/TimesheetRow.test.tsx  | 28 +++++++++++++++++++++++
 src/features/clock/TimesheetRow.tsx       |  5 +++-
 src/features/clock/timesheetUtils.test.ts | 17 ++++++++++++++
 src/features/clock/timesheetUtils.ts      | 12 ++++++++++
 5 files changed, 71 insertions(+), 6 deletions(-)
 create mode 100644 src/features/clock/timesheetUtils.test.ts

diff --git a/src/features/clock/TimesheetPage.tsx b/src/features/clock/TimesheetPage.tsx
index 175532e2..75696538 100644
--- a/src/features/clock/TimesheetPage.tsx
+++ b/src/features/clock/TimesheetPage.tsx
@@ -44,6 +44,7 @@ import {
   fromLocalDateTimeInputValue,
   getDateRange,
   PRESETS,
+  roundDurationSecondsForDisplay,
   toLocalDateTimeInputValue,
   type Preset,
 } from './timesheetUtils';
@@ -499,7 +500,7 @@ export const TimesheetPage: React.FC = () => {
                   Total Hours
                 </Text>
                 <Text size="lg" weight="semibold" data-testid="stat-total-hours">
-                  {formatDuration(filteredSummary.totalSeconds)}
+                  {formatDuration(roundDurationSecondsForDisplay(filteredSummary.totalSeconds))}
                 </Text>
               </CardContent>
             </Card>
@@ -509,7 +510,9 @@ export const TimesheetPage: React.FC = () => {
                   Break Hours
                 </Text>
                 <Text size="lg" weight="semibold">
-                  {formatDuration(filteredSummary.totalBreakSeconds)}
+                  {formatDuration(
+                    roundDurationSecondsForDisplay(filteredSummary.totalBreakSeconds),
+                  )}
                 </Text>
               </CardContent>
             </Card>
@@ -529,7 +532,9 @@ export const TimesheetPage: React.FC = () => {
                   Avg Session
                 </Text>
                 <Text size="lg" weight="semibold">
-                  {formatDuration(filteredSummary.averageSessionSeconds)}
+                  {formatDuration(
+                    roundDurationSecondsForDisplay(filteredSummary.averageSessionSeconds),
+                  )}
                 </Text>
               </CardContent>
             </Card>
@@ -656,7 +661,7 @@ export const TimesheetPage: React.FC = () => {
                 Duration
               </Text>
               <Text size="sm" weight="medium">
-                {formatDuration(editDurationSeconds)}
+                {formatDuration(roundDurationSecondsForDisplay(editDurationSeconds))}
               </Text>
             </div>
           )}
@@ -819,7 +824,7 @@ export const TimesheetPage: React.FC = () => {
                 Duration
               </Text>
               <Text size="sm" weight="medium">
-                {formatDuration(newDurationSeconds)}
+                {formatDuration(roundDurationSecondsForDisplay(newDurationSeconds))}
               </Text>
             </div>
           )}
diff --git a/src/features/clock/TimesheetRow.test.tsx b/src/features/clock/TimesheetRow.test.tsx
index f7fcad6f..31e8dc1b 100644
--- a/src/features/clock/TimesheetRow.test.tsx
+++ b/src/features/clock/TimesheetRow.test.tsx
@@ -167,4 +167,32 @@ describe('TimesheetRow', () => {
     fireEvent.click(editButtons[0]);
     expect(onEdit).toHaveBeenCalledWith(session);
   });
+
+  it('rounds duration display to match minute-level timestamps', () => {
+    const session: ClockEvent = {
+      id: 'session-rounded',
+      userId: 'u1',
+      teamId: 'team-1',
+      startTime: new Date('2026-05-19T10:04:30').getTime(),
+      endTime: new Date('2026-05-19T13:04:29').getTime(),
+      accumulatedTime: 0,
+      breaks: [],
+    };
+
+    render(
+      <table>
+        <tbody>
+          <TimesheetRow
+            session={session}
+            teams={[{ id: 'team-1', name: 'Rounded Crew' }]}
+            onEdit={vi.fn()}
+          />
+        </tbody>
+      </table>,
+    );
+
+    expect(screen.getByText('10:04 AM')).toBeTruthy();
+    expect(screen.getByText('1:04 PM')).toBeTruthy();
+    expect(screen.getByText('3h 0m')).toBeTruthy();
+  });
 });
diff --git a/src/features/clock/TimesheetRow.tsx b/src/features/clock/TimesheetRow.tsx
index 7c1375d4..247420e2 100644
--- a/src/features/clock/TimesheetRow.tsx
+++ b/src/features/clock/TimesheetRow.tsx
@@ -13,6 +13,7 @@ import React from 'react';
 
 import { formatDate, formatDuration, formatTime } from '../../lib/timeUtils';
 import { type ClockEvent } from '../../lib/api';
+import { roundDurationSecondsForDisplay } from './timesheetUtils';
 
 // ─── Types ────────────────────────────────────────────────────────────────────
 
@@ -216,7 +217,9 @@ export const TimesheetRow: React.FC<Props> = ({ session, teams, onEdit }) => {
               )}
             </TableCell>
             <TableCell className="font-mono">
-              {row.durationSeconds !== null ? formatDuration(row.durationSeconds) : '—'}
+              {row.durationSeconds !== null
+                ? formatDuration(roundDurationSecondsForDisplay(row.durationSeconds))
+                : '—'}
             </TableCell>
             <TableCell>{showTeam ? teamName : ''}</TableCell>
             <TableCell>
diff --git a/src/features/clock/timesheetUtils.test.ts b/src/features/clock/timesheetUtils.test.ts
new file mode 100644
index 00000000..84f45e4d
--- /dev/null
+++ b/src/features/clock/timesheetUtils.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest';
+
+import { roundDurationSecondsForDisplay } from './timesheetUtils';
+
+describe('roundDurationSecondsForDisplay', () => {
+  it('rounds to the nearest minute for display', () => {
+    expect(roundDurationSecondsForDisplay(2 * 3600 + 59 * 60 + 29)).toBe(2 * 3600 + 59 * 60);
+    expect(roundDurationSecondsForDisplay(2 * 3600 + 59 * 60 + 30)).toBe(3 * 3600);
+  });
+
+  it('returns 0 for null, zero, negative, or non-finite values', () => {
+    expect(roundDurationSecondsForDisplay(null)).toBe(0);
+    expect(roundDurationSecondsForDisplay(0)).toBe(0);
+    expect(roundDurationSecondsForDisplay(-10)).toBe(0);
+    expect(roundDurationSecondsForDisplay(Number.NaN)).toBe(0);
+  });
+});
diff --git a/src/features/clock/timesheetUtils.ts b/src/features/clock/timesheetUtils.ts
index 227a27bd..84ce0b24 100644
--- a/src/features/clock/timesheetUtils.ts
+++ b/src/features/clock/timesheetUtils.ts
@@ -64,6 +64,18 @@ export function fromLocalDateTimeInputValue(value: string): number | null {
   return Number.isNaN(ms) ? null : ms;
 }
 
+/**
+ * Round a second-based duration to the nearest whole minute for display.
+ *
+ * Timesheet timestamps are shown at minute precision, so durations should
+ * follow the same rounding rule to avoid off-by-one-minute displays.
+ */
+export function roundDurationSecondsForDisplay(totalSeconds: number | null): number {
+  const value = typeof totalSeconds === 'number' ? totalSeconds : 0;
+  if (!Number.isFinite(value) || value <= 0) return 0;
+  return Math.round(value / 60) * 60;
+}
+
 export const PRESETS: { key: Preset; label: string }[] = [
   { key: 'today', label: 'Today' },
   { key: 'yesterday', label: 'Yesterday' },
