Story QR-002 : Gestion de site +¶
@assignee: @angeisaac-ui
Module de génération de codes QR : qr-attendance.
Segment : QR-T1-001 + QR-T0-004 (partiel).
Contexte de la marque : [LES DEUX] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite.
Priorité : 2.
Dépend de : QR-001.
Peut être développé en parallèle avec : QR-003, QR-004, QR-005.
Ordre de fusion : Avant QR-006, QR-008.
Complexité estimée : M.
PRD User Stories : US-QR-008.
Objectif¶
Permettre aux RH de gérer les sites de présence (créer, mettre à jour, désactiver) et de générer des codes QR pour chaque site. Chaque site reçoit un code QR unique signé HMAC que les employés scanneront pour pointer à l'arrivée et au départ. Les codes QR peuvent être régénérés (ce qui invalide le précédent) et téléchargés/imprimés avec le branding de la marque. Cette user story crée également la table de configuration du module (avec la clé secrète HMAC), la table de données des employés mise en cache et la table des sites.
Portée du backend¶
Migrations Flyway¶
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__présence_employés_en_mémoire_cache.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__fréquentation_site.sql
:
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);### Entités et enregistrements
AttendanceModuleConfig
(base de données du tenant) :
| 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 |
Données des employés mises en cache (base de données du tenant) :
| 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 (base de données du tenant) :
| 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 |
DTO :
```java 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 ) {} ```### Couche de référentiel
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:- Écoute les événements
EmployeeCreated,EmployeeUpdated,EmployeeTerminated,ManagerChanged(événements d'application Spring Modulith) - Effectue une insertion ou une mise à jour (upsert) de
CachedEmployeeDataà chaque événement
Couche de service¶
SiteService.java:
createSite(UUID tenantId, CreateSiteRequest) -> SiteResponse— Vérifie l'unicité du nom, génère un token HMAC viaQrCodeGenerator, crée le site, publie un événement d'auditlistSites(UUID tenantId) -> CursorPage<SITERESPONSE>— Renvoie tous les sites du tenant, paginés par curseurgetSite(UUID tenantId, UUID siteId) -> SiteResponse— Renvoie un site uniqueupdateSite(UUID tenantId, UUID siteId, UpdateSiteRequest) -> SiteResponse— Mise à jour partielle, vérifie l'unicité du nom s'il a été modifiéregenerateQr(UUID tenantId, UUID siteId) -> SiteResponse— Génère un nouveau token HMAC, met à jour qrToken et qrGeneratedAt, invalide l'ancien token, publie un événement d'auditdeactivateSite(UUID tenantId, UUID siteId) -> SiteResponse— Définit isActive=false
QrCodeGenerator.java:
generateQrCodeImage(QrCodePayload payload) -> byte[]— Génère une image PNG de code QR à l'aide de la bibliothèque ZXing. The payload is in JSON format:{"siteId":"uuid","tenantId":"uuid","token":"hmac-token"}generateHmacToken(UUID tenantId, UUID siteId, String hmacSecret) -> String— HMAC-SHA256 detenantId:siteIdsigné avec leqr_hmac_secretdu locataire
QrCodeValidator.java:
validate(QrCodePayload payload) -> ValidationResult— Valide la signature HMAC par rapport au secret du locataire, vérifie que le tenantId correspond, que le siteId existe et est actif, et que le token correspond au qrToken actuel du site- Renvoie
ValidationResultavec le statut (VALID, INVALID_SIGNATURE, SITE_INACTIVE, TOKEN_EXPIRED, SITE_NOT_FOUND)
Le secret HMAC est généré automatiquement (chaîne hexadécimale de 256 bits via SecureRandom) lors de la première création de AttendanceModuleConfig pour un tenant.
Endpoints API¶
| 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) |
| ### Règles de validation |
nameobligatoire, 100 caractères maximum, unique au sein du tenant. Erreur de nom en double : « Un site portant ce nom existe déjà. »addressfacultatif, 500 caractères maximum- Le site doit exister et appartenir au tenant pour les opérations de mise à jour/régénération
- La régénération du QR code génère un événement d'audit avec un motif facultatif
Considérations relatives à l'architecture multi-tenant¶
- Toutes les tables incluent la colonne
tenant_id - Toutes les requêtes DOIVENT filtrer par
tenant_id - Le secret HMAC est propre à chaque tenant (stocké dans
attendance_module_config) - L'unicité du nom de site s'applique au niveau du tenant
Portée du frontend¶
Pages et routes¶
/qrcontr/sites— Page de liste des sites (RH uniquement)/qrcontr/sites/:siteId— Détails du site + affichage du QR/qrcontr/sites/new— Formulaire de création de site
Composants¶
Les composants respectent les maquettes fonctionnelles 3.9 et 3.10 des spécifications de conception :
SiteListPage.tsx— Liste des fiches de site avec statut QR, badge actif/inactifSiteCreateForm.tsx— Champs nom + adresse, bouton de soumissionSiteDetailPage.tsx— Affichage du code QR, boutons de téléchargement/impression, bouton de régénérationQrCodeCard.tsx— Affiche l'image du code QR avec le logo de la marque (« PAPILLON HR » + nom du site)
États de l'interface utilisateur (TOUS OBLIGATOIRES)¶
| 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." |
Comportement adaptatif¶
- 360px (mobile) : fiches de site empilées sur toute la largeur, affichage du QR code sur toute la largeur
- 768px (tablette) : grille de fiches de site sur 2 colonnes, QR code centré occupant 50 % de la largeur
- 1024px+ (ordinateur de bureau) : barre de navigation latérale, grille sur 3 colonnes, QR code occupant 33 % de la largeur
Interactions¶
- Régénération du QR : fenêtre de confirmation avec le message « L'ancien QR code sera invalide. Vous devrez imprimer et afficher le nouveau. Continuer ? » Deux boutons : « Annuler » + « Régénérer » (style avertissement/orange)
- Après la régénération : notification « Nouveau QR code généré. Pensez à l'imprimer. »
- Téléchargement du QR : déclencheur de téléchargement du navigateur pour le fichier PNG
- Impression du QR : boîte de dialogue d'impression du navigateur avec le QR + le nom du site + le logo de la marque
Critères d'acceptation¶
```gherkin 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 et règles réglementaires¶
Aucune règle spécifique à la gestion du site.
Normes et conventions¶
docs/standards/java-spring-guidelines.md— Enregistrements d'entités, modèles de repositoriesdocs/standards/api-guidelines.md— Enveloppe ApiResponse, pagination basée sur un curseur (ADR-12)docs/standards/database-guidelines.md— Conception de tables multi-tenant, tenant_id dans toutes les requêtesdocs/standards/react-typescript-guidelines.md— Structure des composants, hooks
Exigences de test¶
Tests unitaires¶
- QrCodeGenerator produit un tableau d'octets PNG valide
- QrCodeValidator accepte les HMAC valides, rejette les tokens invalides/expirés
- SiteService.createSite génère un token HMAC unique par site
- SiteService.regenerateQr invalide l'ancien token
Tests d'intégration¶
- POST /sites -> 201, site créé avec un token QR
- POST /sites avec un nom en double -> 409
- POST /sites/{id}/regenerate-qr -> nouveau token, ancien token invalidé
- GET /sites/{id}/qr-code -> renvoie un fichier PNG avec le type de contenu correct
- Isolation des locataires : le locataire A ne peut pas voir les sites du locataire B
Ce que le contrôle qualité va valider¶
- La page de liste des sites s'affiche correctement sur tous les points de rupture (360px, 768px, 1024px)
- Le code QR s'affiche sur la page de détail du site
- Le téléchargement du QR produit un fichier PNG valide
- L'impression du QR ouvre la boîte de dialogue d'impression du navigateur avec le logo de la marque
- La régénération du QR affiche une confirmation et génère un nouveau code
- La création d'un site affiche une notification de réussite et une carte apparaît
- L'état vide affiche l'illustration d'intégration
- L'état hors ligne affiche la liste mise en cache et bloque la création/modification
Hors du périmètre¶
- Attribution de sites aux employés (QR-008)
- Enregistrement des événements d'horloge (QR-006)
- Configuration CRUD du module au-delà du secret HMAC (QR-004)
- Lecture de QR hors ligne (QR-006)
Définition de « Terminé »¶
- [ ] Tous les endpoints backend sont implémentés et renvoient des réponses correctes
- [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px et 1024px
- [ ] Les 5 états de l'interface utilisateur sont implémentés (chargement, vide, erreur, hors ligne, succès)
- [ ] Les micro-textes en français correspondent exactement aux spécifications de conception
- [ ] Tous les critères d'acceptation sont validés manuellement
- [ ] Tests unitaires rédigés et réussis
- [ ] Tests d'intégration rédigés et réussis
- [ ] Isolation multi-tenant vérifiée
- [ ] Écriture et synchronisation hors ligne : N/A (la gestion du site se fait uniquement en ligne)
- [ ] Pas de
anyen TypeScript, pas de types bruts en Java - [ ] Les réponses API respectent le format d'enveloppe standard
- [ ] Entrées de piste d'audit pour la création, la mise à jour et la régénération des QR du site