Aller au contenu

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 name required, max 100 chars
  • Each day: if isWorkingDay=true, startTime and endTime are required
  • endTime must be after startTime. Error: "L'heure de fin doit etre apres l'heure de debut."
  • breakEndTime must be after breakStartTime
  • breakStartTime / breakEndTime must be within the startTime-endTime window
  • dayOfWeek must be 1-7, no duplicates per schedule
  • Sunday (7) defaults to isWorkingDay=false but can be set to true

Multi-Tenant Considerations

  • All tables include tenant_id column
  • All queries MUST filter by tenant_id
  • Schedule assignment resolution uses CachedEmployeeData.departmentId for 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 counts
  • ScheduleForm.tsx — Per-day configuration (Mon-Sun), name field
  • ScheduleDayRow.tsx — Toggle working day, time fields, break fields, break tracking toggle
  • ScheduleAssignmentPanel.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 layer
  • docs/standards/api-guidelines.md — ApiResponse envelope, validation error format
  • docs/standards/database-guidelines.md — Multi-tenant table design, tenant_id in all queries
  • docs/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