Story QR-018: Overtime Report + Weekly Classification¶
Module: qr-attendance Slice: QR-T4-003 + QR-T5-003 Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 18 Depends on: QR-006 (DailyAttendance), QR-004 (overtime config) Can develop concurrently with: QR-017 Merge order: After QR-004, QR-006 Estimated complexity: M PRD User Stories: US-QR-012
Objective¶
Implement weekly overtime classification and the overtime report. A scheduled batch job runs weekly to classify overtime hours into regulatory bands (BAND_1: 40-46h, BAND_2: 46-48h, BAND_3: >48h) with supplementary flags for night work (21:00-05:00) and Sunday/holiday work. The overtime report presents per-employee per-week band breakdowns for HR and accountants to use in payroll reconciliation. Overtime is stored as minutes (not monetary) — payroll applies rates separately.
Backend Scope¶
Entities & Records¶
WeeklyOvertimeSummary:
- id (UUID)
- tenantId (UUID)
- employeeId (UUID)
- weekStartDate (LocalDate)
- weekEndDate (LocalDate)
- totalWorkedMinutes (int)
- band1Minutes (int)
- band2Minutes (int)
- band3Minutes (int)
- nightMinutes (int)
- sundayHolidayMinutes (int)
- totalOvertimeMinutes (int)
- createdAt (Instant)
- updatedAt (Instant)
Flyway Migrations¶
V001_017__attendance_weekly_overtime_summary.sql:
CREATE TABLE attendance_weekly_overtime_summary (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
employee_id UUID NOT NULL,
week_start_date DATE NOT NULL,
week_end_date DATE NOT NULL,
total_worked_minutes INTEGER NOT NULL DEFAULT 0,
band_1_minutes INTEGER NOT NULL DEFAULT 0,
band_2_minutes INTEGER NOT NULL DEFAULT 0,
band_3_minutes INTEGER NOT NULL DEFAULT 0,
night_minutes INTEGER NOT NULL DEFAULT 0,
sunday_holiday_minutes INTEGER NOT NULL DEFAULT 0,
total_overtime_minutes INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, employee_id, week_start_date)
);
CREATE INDEX idx_weekly_overtime_tenant ON attendance_weekly_overtime_summary(tenant_id, employee_id, week_start_date);
Repository Layer¶
WeeklyOvertimeSummaryRepository:
- findByTenantIdAndEmployeeIdAndWeekStartDate(tenantId, employeeId, weekStart) — for upsert
- findByTenantIdAndWeekStartDateBetween(tenantId, from, to) — for report
- findByTenantIdAndEmployeeIdAndWeekStartDateBetween(tenantId, employeeId, from, to) — per employee
Service Layer¶
OvertimeClassificationService:
- classifyWeek(tenantId, employeeId, weekStart, weekEnd):
- Total weekly worked = sum of DailyAttendance.workedMinutes for the week
- If total > 2400 min (40h): overtime = total - 2400
- Classify into bands (from OvertimeConfig):
- BAND_1: first 360 min of overtime (40h-46h)
- BAND_2: next 120 min (46h-48h)
- BAND_3: remaining (>48h)
- NIGHT: hours worked between nightStartTime-nightEndTime (21:00-05:00 default) — supplementary flag (added on top of band classification)
- SUNDAY_HOLIDAY: hours on Sunday or public holiday — supplementary flag
- Upsert WeeklyOvertimeSummary
- Publish WeeklyOvertimeComputed
- Week runs from configured start day (default Monday 00:00) to day before next week start
OvertimeClassificationScheduledJob:
- @Scheduled weekly (Monday 00:30)
- Iterates active tenants with QRCONTR module enabled
- Classifies previous week for all employees with clock events during that week
OvertimeReportService:
- generateOvertimeReport(tenantId, month, weekStart, employeeId, departmentId):
- Returns OvertimeReport — per employee per week band breakdown
- Uses WeeklyOvertimeSummary table
Retroactive recalculation:
- When a regularization (QR-012) affects a past week, re-run classifyWeek for that week
- Listens to RegularizationApplied and checks if affected DailyAttendance falls in a past week
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/qrcontr/reports/overtime |
?month=2026-03&weekStart=&employeeId=&departmentId= |
ApiResponse<OvertimeReport> |
HR, Comptable |
| GET | /api/v1/qrcontr/reports/export |
?type=overtime&month=&format=xlsx\|csv |
File download (Content-Disposition) | HR, Comptable |
DTOs¶
OvertimeReport:
- month (YearMonth)
- employees (ListemployeeId (UUID)
- name (String)
- weekNumber (int)
- weekStart (LocalDate)
- band1Minutes (int)
- band2Minutes (int)
- band3Minutes (int)
- nightMinutes (int)
- sundayHolidayMinutes (int)
- totalOvertimeMinutes (int)
Validation Rules¶
monthformat: YYYY-MM (required for report)formatmust bexlsxorcsvfor export- Band configuration read from tenant's
OvertimeConfig(QR-004)
Multi-Tenant Considerations¶
All queries filter by tenant_id. Scheduled job iterates tenants independently. Band thresholds are tenant-configurable. HR and Comptable see all within tenant.
Frontend Scope¶
Pages & Routes¶
/qrcontr/reports/overtime— Overtime report view
Components¶
OvertimeReportPage:
- Month/week selector, employee/department filter, overtime table
OvertimeTable:
- Per employee per week: Band 1, Band 2, Band 3, Night, Sunday/Holiday, Total
- Sortable columns
- Totals row at bottom
Reuses ExportButtons from QR-017:
- "Exporter Excel" + "Exporter CSV"
UI States¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton table | — |
| Empty | No overtime for period | "Aucune heure supplementaire pour la periode selectionnee." |
| Error | Inline retry | "Impossible de generer le rapport. Reessayez." |
| Offline | Cached summaries only | "Rapport complet disponible en ligne uniquement." |
| Success | Overtime table rendered | — |
Responsive Behavior¶
- 360px: Horizontal scroll table, 3 columns visible, sticky first column (employee name)
- 768px: 5 columns visible
- 1024px+: Full table with all columns
Interactions¶
- Month/week selector: dropdown for month, week toggle within month
- Employee/department filter: dropdown selectors
- Export: reuses export flow from QR-017 (browser download)
- Table: horizontal scroll on mobile, sticky first column
Acceptance Criteria¶
AC-001: Overtime report by band per employee per week
Given I generate the overtime report
When I view it
Then I see per employee per week: Band 1 hours, Band 2 hours, Band 3 hours, Night hours, Sunday/holiday hours, Total overtime hours
AC-002: Overtime report export for payroll
Given I export the report
When Payroll receives it
Then the data is structured for direct import (Excel/CSV with correct formatting)
OHADA & Regulatory Rules¶
- CI Code du Travail Art. 21.2-21.3: Overtime regulated by decree, 40h/week base
- CI defaults (TV-QR-002): BAND_1 40-46h, BAND_2 46-48h, BAND_3 >48h
- NIGHT: 21:00-05:00 (supplementary classification, added on top of band)
- SUNDAY_HOLIDAY: supplementary classification (added on top of band)
- Bands are configurable per tenant — incorrect defaults correctable without code change
- Overtime hours stored as INTEGER minutes (not monetary — payroll applies rates)
Standards & Conventions¶
docs/standards/java-spring-guidelines.md—@Scheduledpatternsdocs/standards/api-guidelines.mddocs/standards/react-typescript-guidelines.mddocs/standards/database-guidelines.md— Flyway migration naming
Testing Requirements¶
Unit Tests¶
OvertimeClassificationService: no overtime for <= 40h week (2400 min)OvertimeClassificationService: 43h week → 3h BAND_1, 0 BAND_2, 0 BAND_3OvertimeClassificationService: 47h week → 6h BAND_1, 1h BAND_2, 0 BAND_3OvertimeClassificationService: 50h week → 6h BAND_1, 2h BAND_2, 2h BAND_3OvertimeClassificationService: night detection (hours between 21:00-05:00)OvertimeClassificationService: Sunday/holiday detectionOvertimeClassificationService: retroactive recalculation on regularizationOvertimeReportService: aggregation per employee per week
Integration Tests¶
- Weekly job execution: runs for all tenants, classifies correctly
- GET
/reports/overtime?month=2026-03→ correct band breakdown - GET
/reports/export?type=overtime&format=xlsx→ file download - GET
/reports/export?type=overtime&format=csv→ file download with BOM - Non-HR/Comptable → 403
- Tenant isolation: tenant A data does not appear in tenant B report
What QA Will Validate¶
- Verify overtime bands match configuration (40h base, CI defaults)
- Generate report for month with known overtime, verify band breakdown
- Export Excel and CSV, verify formatting and data accuracy
- Verify retroactive recalculation after regularization
- Responsive table at all breakpoints
Out of Scope¶
- Monetary overtime calculation (Payroll module)
- Multi-country overtime rates (V2)
- Collective agreement overrides (V2)
- Real-time overtime alerts (V2)
Definition of Done¶
- [ ] Weekly classification batch job runs correctly
- [ ] Overtime bands classified according to tenant configuration
- [ ] Retroactive recalculation on regularization works
- [ ]
WeeklyOvertimeComputedpublished after classification - [ ] Overtime report endpoint returns correct band breakdown
- [ ] Export reuses Excel/CSV services from QR-017
- [ ] Flyway migration applied successfully
- [ ] Report view renders at all breakpoints (360px, 768px, 1024px)
- [ ] All 5 UI states implemented (loading, empty, error, offline, success)
- [ ] French micro-copy matches design spec exactly
- [ ] All acceptance criteria pass manually
- [ ] Unit tests written and passing
- [ ] Integration tests written and passing
- [ ] Multi-tenant isolation verified
- [ ] No TypeScript
any, no Java raw types - [ ] API responses follow standard envelope format
- [ ]
./gradlew testis green - [ ]
./gradlew spotlessApplypasses