Aller au contenu

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) -> boolean
  • CachedEmployeeRepository.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 via QrCodeGenerator, crée le site, publie un événement d'audit
  • listSites(UUID tenantId) -> CursorPage<SITERESPONSE> — Renvoie tous les sites du tenant, paginés par curseur
  • getSite(UUID tenantId, UUID siteId) -> SiteResponse — Renvoie un site unique
  • updateSite(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'audit
  • deactivateSite(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 de tenantId:siteId signé avec le qr_hmac_secret du 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 ValidationResult avec 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
  • name obligatoire, 100 caractères maximum, unique au sein du tenant. Erreur de nom en double : « Un site portant ce nom existe déjà. »
  • address facultatif, 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/inactif
  • SiteCreateForm.tsx — Champs nom + adresse, bouton de soumission
  • SiteDetailPage.tsx — Affichage du code QR, boutons de téléchargement/impression, bouton de régénération
  • QrCodeCard.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 repositories
  • docs/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êtes
  • docs/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 any en 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