Story QR-003: Work Schedule Configuration + Assignment¶
@assignee: @m-kouassi
Module: qr-attendance Slice: QR-T1-002 Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 3 Depends on: QR-001 Can develop concurrently with: QR-002, QR-004, QR-005 Merge order: Before QR-006 Estimated complexity: M PRD User Stories: US-QR-009
Objective¶
Allow HR to create and manage work schedules with per-day time configuration (start, end, break windows, break tracking toggle), then assign schedules to individual employees or entire departments. Employee-level assignments override department-level defaults. Schedules are the foundation for late detection, overtime calculation, and worked-hours computation in downstream stories.
Backend Scope¶
Flyway Migrations¶
V001_004__attendance_work_schedule.sql:
CREATE TABLE attendance_work_schedule (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name VARCHAR(100) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_work_schedule_tenant ON attendance_work_schedule(tenant_id);
CREATE TABLE attendance_work_schedule_day (
id UUID PRIMARY KEY,
schedule_id UUID NOT NULL REFERENCES attendance_work_schedule(id),
day_of_week SMALLINT NOT NULL CHECK (day_of_week BETWEEN 1 AND 7),
is_working_day BOOLEAN NOT NULL,
start_time TIME,
end_time TIME,
break_start_time TIME,
break_end_time TIME,
break_tracking_enabled BOOLEAN NOT NULL DEFAULT false,
UNIQUE(schedule_id, day_of_week)
);
V001_005__attendance_schedule_assignment.sql:
CREATE TABLE attendance_employee_schedule_assignment (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
employee_id UUID NOT NULL,
schedule_id UUID NOT NULL REFERENCES attendance_work_schedule(id),
effective_from DATE NOT NULL,
effective_to DATE,
UNIQUE(tenant_id, employee_id, effective_from)
);
CREATE INDEX idx_emp_schedule_tenant ON attendance_employee_schedule_assignment(tenant_id, employee_id, effective_from, effective_to);
CREATE TABLE attendance_department_schedule_assignment (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
department_id UUID NOT NULL,
schedule_id UUID NOT NULL REFERENCES attendance_work_schedule(id),
effective_from DATE NOT NULL,
effective_to DATE,
UNIQUE(tenant_id, department_id, effective_from)
);
CREATE INDEX idx_dept_schedule_tenant ON attendance_department_schedule_assignment(tenant_id, department_id, effective_from, effective_to);
Entities & Records¶
WorkSchedule (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID | PK |
| tenantId | UUID | NOT NULL |
| name | String | NOT NULL, max 100 |
| isActive | boolean | NOT NULL, default true |
| createdAt | OffsetDateTime | NOT NULL |
| updatedAt | OffsetDateTime | NOT NULL |
WorkScheduleDay (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID | PK |
| scheduleId | UUID | NOT NULL, FK to WorkSchedule |
| dayOfWeek | short | NOT NULL, 1=Mon..7=Sun |
| isWorkingDay | boolean | NOT NULL |
| startTime | LocalTime | Required if isWorkingDay=true |
| endTime | LocalTime | Required if isWorkingDay=true |
| breakStartTime | LocalTime | Nullable |
| breakEndTime | LocalTime | Nullable |
| breakTrackingEnabled | boolean | NOT NULL, default false |
EmployeeScheduleAssignment (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID | PK |
| tenantId | UUID | NOT NULL |
| employeeId | UUID | NOT NULL |
| scheduleId | UUID | NOT NULL, FK to WorkSchedule |
| effectiveFrom | LocalDate | NOT NULL |
| effectiveTo | LocalDate | Nullable (null = ongoing) |
DepartmentScheduleAssignment (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID | PK |
| tenantId | UUID | NOT NULL |
| departmentId | UUID | NOT NULL |
| scheduleId | UUID | NOT NULL, FK to WorkSchedule |
| effectiveFrom | LocalDate | NOT NULL |
| effectiveTo | LocalDate | Nullable (null = ongoing) |
DTOs:
public record CreateScheduleRequest(
@NotBlank @Size(max = 100) String name,
@NotEmpty List<ScheduleDayRequest> days
) {}
public record ScheduleDayRequest(
@NotNull short dayOfWeek,
@NotNull boolean isWorkingDay,
LocalTime startTime,
LocalTime endTime,
LocalTime breakStartTime,
LocalTime breakEndTime,
boolean breakTrackingEnabled
) {}
public record UpdateScheduleRequest(
@Size(max = 100) String name,
List<ScheduleDayRequest> days,
Boolean isActive
) {}
public record ScheduleResponse(
UUID id,
String name,
boolean isActive,
List<ScheduleDayResponse> days,
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {}
public record ScheduleDayResponse(
short dayOfWeek,
boolean isWorkingDay,
LocalTime startTime,
LocalTime endTime,
LocalTime breakStartTime,
LocalTime breakEndTime,
boolean breakTrackingEnabled
) {}
public record AssignEmployeeRequest(
@NotNull UUID employeeId,
@NotNull LocalDate effectiveFrom,
LocalDate effectiveTo
) {}
public record AssignDepartmentRequest(
@NotNull UUID departmentId,
@NotNull LocalDate effectiveFrom,
LocalDate effectiveTo
) {}
Repository Layer¶
WorkScheduleRepository.java:findByTenantId(UUID tenantId) -> List<WorkSchedule>findByTenantIdAndId(UUID tenantId, UUID id) -> Optional<WorkSchedule>WorkScheduleDayRepository.java:findByScheduleId(UUID scheduleId) -> List<WorkScheduleDay>EmployeeScheduleAssignmentRepository.java:findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> List<EmployeeScheduleAssignment>findActiveByTenantIdAndEmployeeIdAndDate(UUID tenantId, UUID employeeId, LocalDate date) -> Optional<EmployeeScheduleAssignment>— Finds assignment where effectiveFrom <= date AND (effectiveTo IS NULL OR effectiveTo >= date)DepartmentScheduleAssignmentRepository.java:findByTenantIdAndDepartmentId(UUID tenantId, UUID departmentId) -> List<DepartmentScheduleAssignment>findActiveByTenantIdAndDepartmentIdAndDate(UUID tenantId, UUID departmentId, LocalDate date) -> Optional<DepartmentScheduleAssignment>
Service Layer¶
ScheduleService.java:
- createSchedule(UUID tenantId, CreateScheduleRequest) -> ScheduleResponse — Creates schedule with all day configurations, publishes audit event
- listSchedules(UUID tenantId) -> List<ScheduleResponse> — Returns all schedules with their day configurations
- getSchedule(UUID tenantId, UUID scheduleId) -> ScheduleResponse — Returns single schedule with days
- updateSchedule(UUID tenantId, UUID scheduleId, UpdateScheduleRequest) -> ScheduleResponse — Updates schedule name, active status, and/or day configurations
ScheduleAssignmentService.java:
- assignToEmployee(UUID tenantId, UUID scheduleId, AssignEmployeeRequest) -> void — Creates employee-level assignment
- assignToDepartment(UUID tenantId, UUID scheduleId, AssignDepartmentRequest) -> void — Creates department-level assignment
- getEffectiveSchedule(UUID tenantId, UUID employeeId, LocalDate date) -> ScheduleResponse — Resolution logic:
1. Check for employee-level assignment active on date
2. If none, look up employee's departmentId (from CachedEmployeeData), check department-level assignment active on date
3. If neither found, return null (no schedule assigned)
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| POST | /api/v1/qrcontr/schedules |
CreateScheduleRequest | ApiResponse |
HR (P2) |
| GET | /api/v1/qrcontr/schedules |
— | ApiResponse
|
HR (P2) |
| GET | /api/v1/qrcontr/schedules/{scheduleId} |
— | ApiResponse |
HR (P2) |
| PUT | /api/v1/qrcontr/schedules/{scheduleId} |
UpdateScheduleRequest | ApiResponse |
HR (P2) |
| POST | /api/v1/qrcontr/schedules/{scheduleId}/assign-employee |
AssignEmployeeRequest | ApiResponse |
HR (P2) |
| POST | /api/v1/qrcontr/schedules/{scheduleId}/assign-department |
AssignDepartmentRequest | ApiResponse |
HR (P2) |
| GET | /api/v1/qrcontr/employees/{employeeId}/schedule |
— | ApiResponse |
HR, Manager, Employee (own) |
Validation Rules¶
- Schedule
namerequired, max 100 chars - Each day: if
isWorkingDay=true,startTimeandendTimeare required endTimemust be afterstartTime. Error: "L'heure de fin doit etre apres l'heure de debut."breakEndTimemust be afterbreakStartTimebreakStartTime/breakEndTimemust be within thestartTime-endTimewindowdayOfWeekmust be 1-7, no duplicates per schedule- Sunday (7) defaults to
isWorkingDay=falsebut can be set to true
Multi-Tenant Considerations¶
- All tables include
tenant_idcolumn - All queries MUST filter by
tenant_id - Schedule assignment resolution uses
CachedEmployeeData.departmentIdfor department fallback - Employee override takes precedence over department assignment at all times
Frontend Scope¶
Pages & Routes¶
/qrcontr/schedules— Schedule list page (HR only)/qrcontr/schedules/new— Create schedule form/qrcontr/schedules/:scheduleId— Edit schedule + assignment
Components¶
Components follow design spec wireframe 3.11:
ScheduleListPage.tsx— List of schedules with assignment countsScheduleForm.tsx— Per-day configuration (Mon-Sun), name fieldScheduleDayRow.tsx— Toggle working day, time fields, break fields, break tracking toggleScheduleAssignmentPanel.tsx— Assign to employee/department picker
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton for schedule list | — |
| Empty | Guidance illustration | "Aucun horaire configure. Definissez un horaire pour activer le suivi des retards et heures supplementaires." |
| Error | Field-level validation | "L'heure de fin doit etre apres l'heure de debut." |
| Offline | View cached schedules, create/edit disabled | "Disponible en ligne uniquement" |
| Success | Schedule card slides in | "Horaire cree." / "Horaire mis a jour." |
Responsive Behavior¶
- 360px (mobile): Full-width vertical form, one day at a time (accordion)
- 768px (tablet): Full-width form, all days visible, scrollable
- 1024px+ (desktop): Side-by-side layout — schedule list left, form right
Interactions¶
- Day toggle: "Jour travaille" checkbox toggles time fields visibility (slide animation, 200ms)
- Time fields: pre-filled with smart defaults (08:00-17:00, break 12:00-14:00)
- "Appliquer a tous les jours" button: copies current day's times to all enabled days
- Auto-save: fields save on blur, green check appears briefly
Acceptance Criteria¶
AC-001: Per-day schedule configuration
Given I am HR creating a new schedule named "Bureau Standard"
When I configure it
Then I can set different start/end times for each day (Monday through Saturday)
And Sunday is off by default
AC-002: Lunch break window configuration
Given I am configuring a work schedule
When I set up a day
Then I can define a lunch break window with start and end times for calculation purposes
AC-003: Break tracking toggle
Given I am configuring a work schedule
When I configure a day
Then I can enable or disable break tracking (4 scans/day when enabled)
AC-004: Schedule assignment to employee or department
Given I have created a work schedule
When I assign it
Then I can assign it to individual employees or to a department
And all employees in that department inherit the schedule unless they have an individual override
OHADA & Regulatory Rules¶
- CI Code du Travail Art. 21.2: Legal weekly duration = 40 hours. Schedules should be designed around this baseline.
- Sunday work: allowed but classified as SUNDAY_HOLIDAY overtime band.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— Entity records, repository patterns, service layerdocs/standards/api-guidelines.md— ApiResponse envelope, validation error formatdocs/standards/database-guidelines.md— Multi-tenant table design, tenant_id in all queriesdocs/standards/react-typescript-guidelines.md— Component structure, form patterns, hooks
Testing Requirements¶
Unit Tests¶
- Schedule day validation: isWorkingDay=true requires startTime/endTime
- Time range validation: endTime must be after startTime
- Break time must be within start-end window
- Effective schedule resolution: employee assignment overrides department assignment
- Effective schedule resolution: department fallback when no employee assignment
Integration Tests¶
- POST /schedules -> 201, schedule created with days
- GET /schedules -> returns all schedules with day configurations
- PUT /schedules/{id} -> updates schedule and days
- POST /schedules/{id}/assign-employee -> creates assignment
- POST /schedules/{id}/assign-department -> creates assignment
- GET /employees/{id}/schedule -> returns effective schedule (employee override > department)
- Tenant isolation: tenant A cannot see tenant B's schedules
What QA Will Validate¶
- Schedule list page displays correctly on all breakpoints (360px, 768px, 1024px)
- Create schedule form with per-day configuration
- Day toggle hides/shows time fields
- "Appliquer a tous les jours" copies times to all days
- Assignment to employee and department
- Verify employee override takes precedence over department
- Empty state shows guidance message
- Offline state shows cached data, blocks edits
Out of Scope¶
- Clock event recording (QR-006)
- Late/early detection using schedules (QR-009)
- Overtime calculation using schedules (QR-018)
- Bulk schedule assignment (Enterprise V2)
Definition of Done¶
- [ ] All backend endpoints implemented and returning correct responses
- [ ] All frontend pages render correctly at 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
- [ ] Offline write+sync: N/A (schedule management is online-only)
- [ ] No TypeScript
any, no Java raw types - [ ] API responses follow standard envelope format
- [ ] Audit trail entries for schedule creation, update, assignment changes