Story QR-002: Site Management + QR Code Generation¶
@assignee: @angeisaac-ui
Module: qr-attendance Slice: QR-T1-001 + QR-T0-004 (partial) Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 2 Depends on: QR-001 Can develop concurrently with: QR-003, QR-004, QR-005 Merge order: Before QR-006, QR-008 Estimated complexity: M PRD User Stories: US-QR-008
Objective¶
Allow HR to manage attendance sites (create, update, deactivate) and generate QR codes for each site. Each site gets a unique HMAC-signed QR code that employees will scan for clock-in/out. QR codes can be regenerated (invalidating the previous one) and downloaded/printed with brand labeling. This story also creates the module configuration table (with HMAC secret), the cached employee data table, and the site table.
Backend Scope¶
Flyway Migrations¶
V001_001__attendance_module_config.sql:
CREATE TABLE attendance_module_config (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
qr_hmac_secret VARCHAR(255) NOT NULL,
default_late_tolerance_minutes INTEGER NOT NULL DEFAULT 5,
overtime_week_start_day SMALLINT NOT NULL DEFAULT 1,
min_remote_work_advance_days INTEGER NOT NULL DEFAULT 2,
max_remote_work_per_week INTEGER,
max_remote_work_per_month INTEGER,
auto_close_time TIME NOT NULL DEFAULT '23:59',
delegues_consultation_done BOOLEAN NOT NULL DEFAULT false,
delegues_consultation_date DATE,
delegues_pv_document_id UUID,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id)
);
V001_002__attendance_cached_employee.sql:
CREATE TABLE attendance_cached_employee (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
employee_id UUID NOT NULL,
employee_matricule VARCHAR(50),
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
department_id UUID,
current_manager_id UUID,
hire_date DATE,
employee_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
synced_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, employee_id)
);
CREATE INDEX idx_cached_employee_tenant ON attendance_cached_employee(tenant_id, employee_id);
V001_003__attendance_site.sql:
CREATE TABLE attendance_site (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name VARCHAR(100) NOT NULL,
address VARCHAR(500),
qr_token VARCHAR(255) NOT NULL,
qr_generated_at TIMESTAMP NOT NULL DEFAULT NOW(),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, name)
);
CREATE INDEX idx_site_tenant ON attendance_site(tenant_id);
Entities & Records¶
AttendanceModuleConfig (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID | PK |
| tenantId | UUID | NOT NULL, UNIQUE |
| qrHmacSecret | String | NOT NULL, 256-bit hex, auto-generated |
| defaultLateToleranceMinutes | int | NOT NULL, default 5 |
| overtimeWeekStartDay | short | NOT NULL, default 1 (Monday) |
| minRemoteWorkAdvanceDays | int | NOT NULL, default 2 |
| maxRemoteWorkPerWeek | Integer | Nullable |
| maxRemoteWorkPerMonth | Integer | Nullable |
| autoCloseTime | LocalTime | NOT NULL, default 23:59 |
| deleguesConsultationDone | boolean | NOT NULL, default false |
| deleguesConsultationDate | LocalDate | Nullable |
| deleguesPvDocumentId | UUID | Nullable |
| createdAt | OffsetDateTime | NOT NULL |
| updatedAt | OffsetDateTime | NOT NULL |
CachedEmployeeData (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID | PK |
| tenantId | UUID | NOT NULL |
| employeeId | UUID | NOT NULL, UNIQUE with tenantId |
| employeeMatricule | String | Nullable, max 50 |
| firstName | String | NOT NULL, max 100 |
| lastName | String | NOT NULL, max 100 |
| departmentId | UUID | Nullable |
| currentManagerId | UUID | Nullable |
| hireDate | LocalDate | Nullable |
| employeeStatus | String | NOT NULL, default "ACTIVE" |
| syncedAt | OffsetDateTime | NOT NULL |
Site (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID | PK |
| tenantId | UUID | NOT NULL |
| name | String | NOT NULL, max 100, UNIQUE with tenantId |
| address | String | Nullable, max 500 |
| qrToken | String | NOT NULL |
| qrGeneratedAt | OffsetDateTime | NOT NULL |
| isActive | boolean | NOT NULL, default true |
| createdAt | OffsetDateTime | NOT NULL |
| updatedAt | OffsetDateTime | NOT NULL |
DTOs:
public record CreateSiteRequest(
@NotBlank @Size(max = 100) String name,
@Size(max = 500) String address
) {}
public record UpdateSiteRequest(
@Size(max = 100) String name,
@Size(max = 500) String address,
Boolean isActive
) {}
public record SiteResponse(
UUID id,
String name,
String address,
boolean isActive,
OffsetDateTime qrGeneratedAt,
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {}
public record QrCodePayload(
UUID siteId,
UUID tenantId,
String token
) {}
Repository Layer¶
SiteRepository.java:findByTenantId(UUID tenantId) -> List<Site>findByTenantIdAndId(UUID tenantId, UUID id) -> Optional<Site>existsByTenantIdAndName(UUID tenantId, String name) -> booleanCachedEmployeeRepository.java:findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<CachedEmployeeData>findByTenantId(UUID tenantId) -> List<CachedEmployeeData>EmployeeCacheEventListener.java:- Listens for
EmployeeCreated,EmployeeUpdated,EmployeeTerminated,ManagerChanged(Spring Modulith Application Events) - Upserts
CachedEmployeeDataon each event
Service Layer¶
SiteService.java:
- createSite(UUID tenantId, CreateSiteRequest) -> SiteResponse — Validates name uniqueness, generates HMAC token via QrCodeGenerator, creates site, publishes audit event
- listSites(UUID tenantId) -> CursorPage<SiteResponse> — Returns all sites for tenant, cursor-paginated
- getSite(UUID tenantId, UUID siteId) -> SiteResponse — Returns single site
- updateSite(UUID tenantId, UUID siteId, UpdateSiteRequest) -> SiteResponse — Partial update, validates name uniqueness if changed
- regenerateQr(UUID tenantId, UUID siteId) -> SiteResponse — Generates new HMAC token, updates qrToken and qrGeneratedAt, invalidates old token, publishes audit event
- deactivateSite(UUID tenantId, UUID siteId) -> SiteResponse — Sets isActive=false
QrCodeGenerator.java:
- generateQrCodeImage(QrCodePayload payload) -> byte[] — Generates QR code PNG using ZXing library. Payload is JSON: {"siteId":"uuid","tenantId":"uuid","token":"hmac-token"}
- generateHmacToken(UUID tenantId, UUID siteId, String hmacSecret) -> String — HMAC-SHA256 of tenantId:siteId signed with tenant's qr_hmac_secret
QrCodeValidator.java:
- validate(QrCodePayload payload) -> ValidationResult — Validates HMAC signature against tenant's secret, checks tenantId matches, siteId exists and is active, token matches site's current qrToken
- Returns ValidationResult with status (VALID, INVALID_SIGNATURE, SITE_INACTIVE, TOKEN_EXPIRED, SITE_NOT_FOUND)
HMAC secret is auto-generated (256-bit hex string via SecureRandom) when AttendanceModuleConfig is first created for a tenant.
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| POST | /api/v1/qrcontr/sites |
CreateSiteRequest | ApiResponse |
HR (P2) |
| GET | /api/v1/qrcontr/sites |
— | ApiResponse |
HR, Manager |
| GET | /api/v1/qrcontr/sites/{siteId} |
— | ApiResponse |
HR, Manager |
| PUT | /api/v1/qrcontr/sites/{siteId} |
UpdateSiteRequest | ApiResponse |
HR (P2) |
| POST | /api/v1/qrcontr/sites/{siteId}/regenerate-qr |
— | ApiResponse |
HR (P2) |
| GET | /api/v1/qrcontr/sites/{siteId}/qr-code |
— | PNG image (byte[]) | HR (P2) |
Validation Rules¶
namerequired, max 100 chars, unique within tenant. Duplicate name error: "Un site avec ce nom existe deja."addressoptional, max 500 chars- Site must exist and belong to tenant for update/regenerate operations
- QR regeneration logs audit event with optional reason
Multi-Tenant Considerations¶
- All tables include
tenant_idcolumn - All queries MUST filter by
tenant_id - HMAC secret is per-tenant (stored in
attendance_module_config) - Site name uniqueness is scoped to tenant
Frontend Scope¶
Pages & Routes¶
/qrcontr/sites— Site list page (HR only)/qrcontr/sites/:siteId— Site detail + QR view/qrcontr/sites/new— Create site form
Components¶
Components follow design spec wireframes 3.9, 3.10:
SiteListPage.tsx— List of site cards with QR status, active/inactive badgeSiteCreateForm.tsx— Name + address fields, submit buttonSiteDetailPage.tsx— QR code display, download/print buttons, regenerate buttonQrCodeCard.tsx— Displays QR code image with brand label ("PAPILLON HR" + site name)
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton cards for site list | — |
| Empty | Onboarding illustration + guidance | "Creez votre premier site de pointage." |
| Error | Inline retry card | "Une erreur est survenue. Reessayez." |
| Offline | Cached site list, create/edit disabled | "Disponible en ligne uniquement" |
| Success (create) | Site card slides in, QR appears | "Site cree avec succes." |
Responsive Behavior¶
- 360px (mobile): Full-width stacked site cards, full-width QR display
- 768px (tablet): 2-column site cards grid, centered QR at 50% width
- 1024px+ (desktop): Sidebar nav, 3-column grid, QR at 33% width
Interactions¶
- QR regeneration: confirmation bottom sheet with message "L'ancien QR code sera invalide. Vous devrez imprimer et afficher le nouveau. Continuer ?" Two buttons: "Annuler" + "Regenerer" (warning/amber style)
- After regeneration: toast "Nouveau QR code genere. Pensez a l'imprimer."
- QR download: browser download trigger for PNG file
- QR print: browser print dialog with QR + site name + brand label
Acceptance Criteria¶
AC-001: Site creation with automatic QR code generation
Given I am HR and logged in
When I create a new site with name "Siege Abidjan" and address "Plateau, Rue du Commerce"
Then the site is saved with a QR code containing the site ID, tenant ID, and a static HMAC-signed token
And the QR code image is generated server-side and available for download/print
AC-002: QR code regeneration invalidates previous code
Given a site "Entrepot Yopougon" exists with an active QR code
When I regenerate the site's QR code as HR
Then the previous QR code is immediately invalidated
And a new HMAC token is generated
And any scan using the old QR code will fail validation
AC-003: QR code download and print
Given a QR code has been generated for a site
When I view the QR code detail page
Then I can download the QR code as a PNG image
And I can print the QR code with the site name and brand label
OHADA & Regulatory Rules¶
None specific to site management.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— Entity records, repository patternsdocs/standards/api-guidelines.md— ApiResponse envelope, cursor-based pagination (ADR-12)docs/standards/database-guidelines.md— Multi-tenant table design, tenant_id in all queriesdocs/standards/react-typescript-guidelines.md— Component structure, hooks
Testing Requirements¶
Unit Tests¶
- QrCodeGenerator produces valid PNG byte array
- QrCodeValidator accepts valid HMAC, rejects invalid/expired tokens
- SiteService.createSite generates unique HMAC token per site
- SiteService.regenerateQr invalidates old token
Integration Tests¶
- POST /sites -> 201, site created with QR token
- POST /sites with duplicate name -> 409
- POST /sites/{id}/regenerate-qr -> new token, old token invalid
- GET /sites/{id}/qr-code -> returns PNG with correct content-type
- Tenant isolation: tenant A cannot see tenant B's sites
What QA Will Validate¶
- Site list page displays correctly on all breakpoints (360px, 768px, 1024px)
- QR code displays on site detail page
- QR download produces a valid PNG
- QR print opens browser print dialog with brand label
- QR regeneration shows confirmation, generates new code
- Create site shows success toast and card appears
- Empty state displays onboarding illustration
- Offline state shows cached list, blocks create/edit
Out of Scope¶
- Employee site assignment (QR-008)
- Clock event recording (QR-006)
- Module configuration CRUD beyond HMAC secret (QR-004)
- Offline QR scanning (QR-006)
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 (site management is online-only)
- [ ] No TypeScript
any, no Java raw types - [ ] API responses follow standard envelope format
- [ ] Audit trail entries for site creation, update, QR regeneration