Story OP-005: Frontend — Liste campagnes + flow « Créer et envoyer » (V0.0.0)¶
Module: operator-console
Slice: Architecture §11 OP-001c
Side: [OPERATOR]
Version target: V0.0.0
Priority: 6 (milestone démo V0.0.0)
Depends on: OP-000 (shell + apiClient + Skeleton), OP-001 (GET /contracts?eligible), OP-002 (POST /campaigns), OP-003 (POST /campaigns/{id}/send)
Can develop concurrently with: OP-004
Merge order: After backend V0.0.0
Estimated complexity: M
PRD User Stories: PRD design §11 OP-001 (Liste campagnes V0.0.0)
Wireframe: docs/wireframes/operator-console/OP-005/ (à générer via /wireframe op OP-005 — brief design §11 OP-001)
Objective¶
Livrer la première UI V0.0.0 utilisable : un opérateur ouvre /campagnes, voit la liste de ses campagnes existantes (vide au début), clique + Nouvelle campagne → modale « X contrats éligibles. Confirmer ? » → la campagne est créée puis envoyée fire-and-forget → toast de succès → la liste rafraîchit. Démontre le flow E2E V0.0.0.
Backend Scope¶
Aucun nouveau endpoint backend. Consomme :
- GET /api/operator/contracts?eligible=true (OP-001)
- GET /api/operator/campaigns?status=&cursor=&pageSize=25 — endpoint ajouté ICI dans cette story (lecture seule, minimal V0.0.0).
- POST /api/operator/campaigns (OP-002)
- POST /api/operator/campaigns/{id}/send (OP-003)
Service Layer (additif ici)¶
CampaignListController.list(...):GET /campaigns?status=&cursor=&pageSize=25retournant{ data: CampaignSummary[], nextCursor, hasMore }avecCampaignSummary = { id, name, status, agencyName, contractCount, sentAt, createdAt }. Pagination curseur (D6).- Filtres V0.0.0 :
status(BROUILLON|ENVOYÉE|CLÔTURÉE|ANNULÉE|all) — autres filtres reportés à V0.0.1+.
API Endpoints (nouveaux ici)¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
GET |
/api/operator/campaigns?status=&cursor=&pageSize=25 |
— | { data, nextCursor, hasMore } |
Tous rôles OP |
Validation Rules¶
status∈ {BROUILLON,ENVOYÉE,CLÔTURÉE,ANNULÉE,all} sinon 400.
Multi-Tenant Considerations¶
- Scope
tenant_idJWT,agency_idnon filtré V0.0.0.
Audit & Logging¶
- Pas d'audit sur la lecture.
Frontend Scope¶
Pages & Routes¶
- Route
/campagnes(TanStack Router) →CampaignListPage.tsx.
Components (sous frontend-operator/src/features/campaigns/)¶
CampaignListPage.tsx— orchestrateur (data fetching + state).CampaignTable.tsx— DataTable étendu (existant) avec colonnes : Nom, Agence, Contrats, Statut, Créée le.StatusPill.tsx— primitive nouvelle, à créer ici (couleurs §8.2 design : Brouillon gris, Envoyée bleu info, Clôturée vert success, Annulée orange warning — JAMAIS rouge).FilterBarSimple.tsx— primitive nouvelle ici (V0.0.0 = filtre statut uniquement ; étendue en V0.0.1+).EmptyState.tsx— primitive nouvelle ici (props :illustration?,title,description,action?).CreateAndSendModal.tsx— modale qui :- Affiche le compte de contrats éligibles (appel GET /contracts?eligible).
- Bouton « Créer et envoyer » : enchaîne POST /campaigns puis POST /campaigns/{id}/send.
- Toast succès « Campagne envoyée à N client(s). »
- Sur erreur : affiche le message FR du backend dans une
InlineBannerrouge. InlineBanner.tsx— primitive nouvelle ici.api/campaigns.ts— fonctionslistCampaigns,createCampaign,sendCampaign,getEligibleContracts.hooks/useCampaigns.ts— React Query (queryKey['campaigns', filters], staleTime 30s).hooks/useCreateAndSend.ts— mutation pipeline.types/Campaign.ts— types TS partagés.
UI States (CampaignListPage)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | 8 rows Skeleton (DataTable). Filter bar interactive. | — |
| Empty (no filter) | EmptyState illustration + CTA | « Aucune campagne pour le moment. Importez des contrats puis créez une campagne ad hoc. » + bouton + Nouvelle campagne |
| Empty (filtered) | EmptyState plus court | « Aucun résultat pour ces filtres. [Effacer les filtres] » |
| Error | InlineBanner rouge au-dessus + Réessayer | « Erreur de chargement. [Réessayer] » |
| Offline / low network | GlobalBanner (OP-000) + bouton + Nouvelle campagne grisé |
« Connexion perdue. Certaines fonctions sont indisponibles. » |
| Success | Liste rendue ; nouveau résultat fade 150 ms | — |
UI States (CreateAndSendModal)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton text "Chargement des contrats éligibles…" | — |
| Empty (0 éligibles) | Bouton « Créer et envoyer » désactivé + message | « Aucun contrat éligible dans la fenêtre actuelle. » |
| Error | InlineBanner | « Impossible de charger les contrats éligibles. [Réessayer] » |
| Offline | Modale entièrement grisée + tooltip | « Indisponible hors-ligne. » |
| Success | Toast 4s + fermeture modale + invalidation queries | « Campagne envoyée à {n} client(s). » |
Responsive Behavior¶
- ≥ 1280 px : layout complet avec sidebar 240 px.
- 1024–1279 px : table conserve toutes les colonnes ; padding réduit.
- 768–1023 px : sidebar repliée en icônes ; modale plein écran (90 % largeur).
- < 768 px : page d'accueil mobile (déjà gérée par OP-000).
Connectivity Behavior¶
N/A — opérateur online-first. Le bouton de création est disabled offline (vérification navigator.onLine + disabled={!isOnline} sur le bouton).
Interactions¶
- Filtre
statusURL-persisté :?status=BROUILLON. Rechargement de la page conserve. - Pagination URL-persistée :
?cursor=.... - Keyboard
/focus la barre de recherche globale (TopBar — préexistante OP-000). - Confirmation modale : Escape ferme, Enter confirme (focus trap).
Acceptance Criteria¶
AC-001: La liste des campagnes affiche les campagnes triées par created_at DESC
Given 3 campagnes existent (1 BROUILLON, 1 ENVOYÉE, 1 CLÔTURÉE)
When un opérateur charge /campagnes
Then la table affiche les 3 rows avec colonnes Nom/Agence/Contrats/Statut/Créée le
And les statuts sont rendus en pills aux couleurs §8.2 (Brouillon=gris, Envoyée=bleu, Clôturée=vert, Annulée=orange)
And le tri par défaut est created_at DESC.
AC-002: Filtre par statut URL-persistant
Given la liste affiche 5 campagnes mélangées
When je sélectionne "BROUILLON" dans le filtre
Then l'URL devient /campagnes?status=BROUILLON
And seules les campagnes BROUILLON apparaissent
And un reload de la page conserve le filtre.
AC-003: Flow « Créer et envoyer » end-to-end V0.0.0
Given 12 contrats éligibles dans la fenêtre actuelle
When je clique "+ Nouvelle campagne" puis "Créer et envoyer" dans la modale
Then la modale affichait "12 contrats éligibles"
And la campagne apparait dans la liste avec statut ENVOYÉE après refresh auto
And un toast success "Campagne envoyée à 12 client(s)." s'affiche 4 s
And aucune erreur console.
AC-004: Cohort vide → bouton désactivé
Given 0 contrat éligible
When j'ouvre la modale "+ Nouvelle campagne"
Then le bouton « Créer et envoyer » est désactivé
And le message "Aucun contrat éligible dans la fenêtre actuelle." est affiché.
Compliance Rules¶
- PRD §C.2 localisation : montants
15 000 XOF(espace milliers, 0 décimale). Vérifier le formatter (déjà dansutils/formatting.tsshell). - PRD design §8.2 : couleurs statut respectées exactement.
- PRD §C.4 connectivité : bandeau offline + boutons d'écriture grisés.
- FR-OP-06 : DOB n'apparait pas dans la liste campagne (pas de DOB dans
CampaignSummary— vérifié).
Standards & Conventions¶
docs/standards/react-typescript-guidelines.md— React 19.2, TanStack Router, React Query, CSS Modules.docs/standards/design-system.md— StatusPill, EmptyState, InlineBanner, FilterBar tokens.docs/standards/design-principles.md— desktop-first, calme, dense.docs/standards/api-guidelines.md— pagination curseur.
Testing Requirements¶
Unit Tests¶
CampaignTableTest(Vitest + RTL) : rendering, tri, pills couleur.StatusPillTest: tous les statuts → bonnes classes CSS.CreateAndSendModalTest: enchaînement create → send, gestion erreur.
Integration Tests¶
- Playwright E2E
flow-create-and-send.spec.tsAC-003 : seed cohort → flow complet → vérification UI. offline-list.spec.ts:context.setOffline(true)→ bandeau + bouton grisé.
What QA Will Validate¶
- Toutes les pages rendent à 360 / 768 / 1024 / 1280 / 1440 px sans débordement.
- Tous les status pills lisibles (contraste WCAG AA).
- Filtres URL survivent au reload.
- Aucune string anglaise visible.
Out of Scope¶
- Édition pré-envoi (drawer contrat) → OP-008 (V0.0.1).
- Détail campagne ENVOYÉE avec KPI temps réel → OP-009 (V0.0.1).
- Création ad hoc avec sélecteur contrats → V1 (US-OP-03).
- Filtres avancés (période, branche, agence) → V0.0.2 (OP-011 ajoute le filtre agence) / V1 (autres).
- Recherche globale TopBar → V1 (US-OP-10).
- Badge URGENT sur les rows → V1 (TD-2).
Definition of Done¶
- [ ] Route
/campagnesrend la liste + filtre statut - [ ] AC-001..AC-004 passent (manuel + Playwright)
- [ ] Toutes les pages rendent à 768 / 1024 / 1280 / 1440 px
- [ ] 5 états UI implémentés (loading, empty, empty-filtered, error, offline, success)
- [ ] French micro-copy = exactement les chaînes design §8.2/§8.3/§8.4/§8.5
- [ ] StatusPill, EmptyState, InlineBanner, FilterBarSimple primitives créées et exportables
- [ ] Filtre statut URL-persisté + reload OK
- [ ] Pagination curseur fonctionnelle
- [ ] Flow create+send chaîné côté frontend (mutation pipeline)
- [ ] Toast success affiché après envoi
- [ ] Boutons d'écriture grisés quand offline
- [ ] Unit tests + Playwright E2E verts
- [ ] Audit log non touché par la lecture (vérifié)
- [ ] Tenant isolation : un operator tenant A ne voit jamais une campagne tenant B (test E2E)