Aller au contenu

Story QR-008: Employee Site Assignment

Module: qr-attendance Slice: QR-T2-002 Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 8 Depends on: QR-002 (sites exist) Can develop concurrently with: QR-006, QR-007 Merge order: Any order relative to QR-006+ Estimated complexity: S PRD User Stories: N/A — supports US-QR-001 (flexible site scanning), US-QR-010 (dashboard site filter)


Objective

Create the employee-to-site assignment model that supports primary and secondary site designations. When an employee scans a QR code at a non-primary site, the clock event is recorded normally but flagged as a non-primary site scan. This enables site-based filtering on the dashboard and supports future geofencing features.


Backend Scope

Entities & Records

EmployeeSiteAssignment: - id (UUID) - tenantId (UUID) - employeeId (UUID) - siteId (UUID) - isPrimary (boolean, default true) - effectiveFrom (LocalDate) - effectiveTo (LocalDate, nullable)

Flyway Migrations

V001_014__attendance_employee_site_assignment.sql:

CREATE TABLE attendance_employee_site_assignment (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    employee_id UUID NOT NULL,
    site_id UUID NOT NULL,
    is_primary BOOLEAN NOT NULL DEFAULT true,
    effective_from DATE NOT NULL,
    effective_to DATE,
    UNIQUE(tenant_id, employee_id, site_id, effective_from)
);
CREATE INDEX idx_emp_site_tenant ON attendance_employee_site_assignment(tenant_id, employee_id, is_primary);

Repository Layer

SiteAssignmentRepository: - findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) — all assignments for an employee - findPrimaryByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) — primary site only - findBySiteId(UUID tenantId, UUID siteId) — all employees assigned to a site

Service Layer

SiteAssignmentService: - assignEmployeeToSite(AssignEmployeeToSiteRequest) — assign employee to site, enforce at most one primary site per employee at a time - removeAssignment(UUID id) — remove an assignment - listAssignments(UUID tenantId, UUID employeeId) — list all assignments for an employee - Non-primary site scan flagging: when a clock event is at a non-primary site, flag in DailyAttendance.anomalyDetails

API Endpoints

Method Path Request Body Response Auth
POST /api/v1/qrcontr/site-assignments AssignEmployeeToSiteRequest ApiResponse<SiteAssignmentResponse> HR (P2)
GET /api/v1/qrcontr/employees/{employeeId}/site-assignments ApiResponse<List<SiteAssignmentResponse>> HR, Manager, Employee (own)
DELETE /api/v1/qrcontr/site-assignments/{id} ApiResponse<Void> HR (P2)

DTOs

AssignEmployeeToSiteRequest: - employeeId (UUID) - siteId (UUID) - isPrimary (boolean) - effectiveFrom (LocalDate) - effectiveTo (LocalDate, nullable)

SiteAssignmentResponse: - All entity fields + siteName (String)

Validation Rules

  • At most one primary site per employee at a time
  • Site must exist and be active
  • effectiveFrom must not be null
  • If effectiveTo is provided, it must be after effectiveFrom

Multi-Tenant Considerations

All queries filter by tenant_id. Cross-tenant site assignments are impossible by design (site IDs are tenant-scoped).


Frontend Scope

Components

SiteAssignmentPanel: - On site detail page, list assigned employees with primary badge - Add employee picker to assign new employees

EmployeeSiteList: - On employee detail page, show assigned sites with primary indicator

UI States

State Behavior French Copy
Loading Skeleton rows
Empty No assignments "Aucun employe assigne a ce site."
Error Inline error "Une erreur est survenue. Reessayez."
Offline Cached data, actions disabled "Disponible en ligne uniquement"
Success Row added with animation "Employe assigne au site."

Responsive Behavior

  • 360px: List view, full-width assignment cards
  • 768px: Same layout, with inline add
  • 1024px+: Table view with bulk assignment (Enterprise V2 placeholder)

Acceptance Criteria

AC-001: Assign employee to primary site
Given I am HR
When I assign employee "KONE Aminata" to site "Siege Abidjan" as primary
Then the assignment is saved and the employee's primary site is "Siege Abidjan"

AC-002: Non-primary site scan flagging
Given employee "KONE Aminata" has primary site "Siege Abidjan"
When she scans the QR code at "Entrepot Yopougon" (a non-primary site)
Then the clock event is recorded normally but flagged as a non-primary site scan

OHADA & Regulatory Rules

None specific to site assignment.


Standards & Conventions

  • docs/standards/java-spring-guidelines.md
  • docs/standards/api-guidelines.md
  • docs/standards/database-guidelines.md

Testing Requirements

Unit Tests

  • Primary uniqueness enforcement: assigning a second primary replaces the first
  • Non-primary flagging logic: clock event at non-primary site sets anomaly flag
  • Validation: effectiveTo before effectiveFrom rejected, missing effectiveFrom rejected

Integration Tests

  • POST site-assignment — 201 with correct response
  • GET employee site-assignments — returns all assignments with site names
  • DELETE site-assignment — 200, assignment removed
  • Primary site constraint: second primary replaces first
  • Tenant isolation: assignments from tenant A not visible to tenant B

What QA Will Validate

  • Assign employee to site, verify primary badge displays
  • Scan at non-primary site, verify flagging in attendance records
  • Verify employee detail page shows assigned sites

Out of Scope

  • Geofencing (V2)
  • Device binding (V2)
  • Bulk assignment (Enterprise V2)

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend components render correctly at 360px, 768px, 1024px
  • [ ] All 5 UI states implemented (loading, empty, error, offline, success)
  • [ ] French micro-copy matches design spec
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests and integration tests passing
  • [ ] Multi-tenant isolation verified
  • [ ] No TypeScript any, no Java raw types
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries for assignment changes
  • [ ] ./gradlew test is green
  • [ ] ./gradlew spotlessApply passes