132 lines
5.8 KiB
Markdown
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
|