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
- {formatDuration(filteredSummary.totalSeconds)}
+ {formatDuration(roundDurationSecondsForDisplay(filteredSummary.totalSeconds))}
@@ -509,7 +510,9 @@ export const TimesheetPage: React.FC = () => {
Break Hours
- {formatDuration(filteredSummary.totalBreakSeconds)}
+ {formatDuration(
+ roundDurationSecondsForDisplay(filteredSummary.totalBreakSeconds),
+ )}
@@ -529,7 +532,9 @@ export const TimesheetPage: React.FC = () => {
Avg Session
- {formatDuration(filteredSummary.averageSessionSeconds)}
+ {formatDuration(
+ roundDurationSecondsForDisplay(filteredSummary.averageSessionSeconds),
+ )}
@@ -656,7 +661,7 @@ export const TimesheetPage: React.FC = () => {
Duration
- {formatDuration(editDurationSeconds)}
+ {formatDuration(roundDurationSecondsForDisplay(editDurationSeconds))}
)}
@@ -819,7 +824,7 @@ export const TimesheetPage: React.FC = () => {
Duration
- {formatDuration(newDurationSeconds)}
+ {formatDuration(roundDurationSecondsForDisplay(newDurationSeconds))}
)}
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(
+
,
+ );
+
+ 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 = ({ session, teams, onEdit }) => {
)}
- {row.durationSeconds !== null ? formatDuration(row.durationSeconds) : '—'}
+ {row.durationSeconds !== null
+ ? formatDuration(roundDurationSecondsForDisplay(row.durationSeconds))
+ : '—'}
{showTeam ? teamName : ''}
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' },