Files
timetracker/docs/features/timer-breaks.md
2026-03-26 09:51:55 +01:00

132 lines
5.8 KiB
Markdown

# Feature: Timer Breaks (Pause During Work)
## Overview
Allow users to take breaks while a timer is running. When on break, elapsed time is frozen and break time is tracked. When resumed, break time accumulates and is subtracted from the displayed work time.
## User Experience
### Timer States
1. **Running** — normal state, elapsed time ticking
2. **On Break** — elapsed time frozen, break time ticking, Stop/Cancel buttons disabled
3. **Stopped** — no timer active
### UI Changes (TimerWidget)
- Add a **"Break"** button (amber, `Pause` icon) next to Stop when timer is running
- When on break:
- Change pulsing dot color from red to amber
- Elapsed time frozen at net work time
- Show break time below: `Break: Xm XXs` (live-ticking)
- Replace "Break" button with **"Resume"** button (green, `Play` icon)
- **Disable** Stop and Cancel buttons (tooltip: "Resume before stopping")
### Duration Calculations
- **Work time (displayed):** `now - startTime - totalBreakSeconds`
- Where `totalBreakSeconds = (breakMinutes * 60) + (now - breakStart if on break)`
- When on break: frozen at `(breakStart - startTime - breakMinutes * 60)`
- **Break time (displayed):** `breakMinutes * 60 + (now - breakStart if on break)`
## Implementation
### 1. Database Schema (`backend/prisma/schema.prisma`)
Add two fields to `OngoingTimer`:
```prisma
model OngoingTimer {
// ... existing fields ...
breakMinutes Int @default(0) @map("break_minutes")
breakStart DateTime? @map("break_start") @db.Timestamptz()
}
```
Run: `npx prisma migrate dev --name add_timer_break_fields`
### 2. Backend Service (`backend/src/services/timer.service.ts`)
**New method `startBreak(userId)`:**
- Get ongoing timer, throw `NotFoundError` if none
- Check `timer.breakStart` is null (not already on break), throw `BadRequestError` if on break
- Update: `breakStart = new Date()`
- Return updated timer
**New method `endBreak(userId)`:**
- Get ongoing timer, throw `NotFoundError` if none
- Check `timer.breakStart` is not null, throw `BadRequestError` if not on break
- Calculate additional break minutes: `Math.floor((now - breakStart) / 60000)`
- Update: `breakMinutes += additionalMinutes`, `breakStart = null`
- Return updated timer
**Modify `stop(userId)`:**
- Before creating time entry, check `timer.breakStart` is null — throw `BadRequestError("Cannot stop timer while on break")` if break is active
- When creating `TimeEntry`, set `breakMinutes: timer.breakMinutes`
**Modify `cancel(userId)`:**
- Check `timer.breakStart` is null — throw `BadRequestError("Cannot cancel timer while on break")` if break is active
### 3. Backend Routes (`backend/src/routes/timer.routes.ts`)
Add two new routes (both require auth, no body validation):
```
POST /api/timer/break → timerService.startBreak(userId)
POST /api/timer/resume → timerService.endBreak(userId)
```
### 4. MCP Tools (`backend/src/routes/mcp.routes.ts`)
Add two MCP tools: `pause_timer` and `resume_timer`.
### 5. Frontend Types (`frontend/src/types/index.ts`)
Update `OngoingTimer` interface:
```typescript
export interface OngoingTimer {
// ... existing fields ...
breakMinutes: number;
breakStart: string | null;
}
```
### 6. Frontend API (`frontend/src/api/timer.ts`)
Add two methods:
```typescript
startBreak: async (): Promise<OngoingTimer> => { ... }
endBreak: async (): Promise<OngoingTimer> => { ... }
```
### 7. Frontend TimerContext (`frontend/src/contexts/TimerContext.tsx`)
- Add `breakSeconds` state (live-updating, similar to `elapsedSeconds`)
- Expose `isOnBreak` derived boolean (`ongoingTimer?.breakStart !== null`)
- Update elapsed time calculation:
- Running: `(now - startTime) - (breakMinutes * 60) - (now - breakStart if on break)`
- On break: `(breakStart - startTime) - (breakMinutes * 60)` (frozen)
- Break seconds: `(breakMinutes * 60) + (now - breakStart if on break)`
- Add `startBreak()` and `endBreak()` callbacks
- Expose `breakSeconds` and `isOnBreak` in context value
### 8. Frontend TimerWidget (`frontend/src/components/TimerWidget.tsx`)
- Import `Pause` icon from lucide-react
- Add Break/Resume button between project selector and Stop button
- Show break time display when `breakSeconds > 0` or `isOnBreak`
- Change dot color to amber when on break
- Disable Stop/Cancel when on break with tooltip
## Files to Modify (in order)
| # | File | Change |
|---|------|--------|
| 1 | `backend/prisma/schema.prisma` | Add `breakMinutes`, `breakStart` to `OngoingTimer` |
| 2 | `backend/src/services/timer.service.ts` | Add `startBreak()`, `endBreak()`, modify `stop()` and `cancel()` |
| 3 | `backend/src/routes/timer.routes.ts` | Add `/break` and `/resume` routes |
| 4 | `backend/src/routes/mcp.routes.ts` | Add `pause_timer` and `resume_timer` MCP tools |
| 5 | `frontend/src/types/index.ts` | Add `breakMinutes`, `breakStart` to `OngoingTimer` |
| 6 | `frontend/src/api/timer.ts` | Add `startBreak()`, `endBreak()` API methods |
| 7 | `frontend/src/contexts/TimerContext.tsx` | Add break state, `breakSeconds`, `isOnBreak`, break methods |
| 8 | `frontend/src/components/TimerWidget.tsx` | Add break UI (button, display, disabled states) |
## Edge Cases
- Break start must be after timer start (always true since break is clicked after start)
- Break duration naturally cannot exceed work duration (breakStart > startTime)
- On stop: reject if break is active (user must resume first)
- On cancel: reject if break is active (user must resume first)
- Break minutes accumulate across multiple break/resume cycles
- Timer refetch (every 60s) will sync break state from server
## Verification
- Run `npm run lint` in both `frontend/` and `backend/`
- Run `npm run build` in both `frontend/` and `backend/`
- Manual testing: start timer → break → verify elapsed frozen, break ticking → resume → verify break added to total → stop → verify time entry has correct breakMinutes