Story OP-008: Frontend — Détail campagne BROUILLON v2 (drawer édition + garde anomalies)¶
Module: operator-console Slice: Architecture §11 OP-002 (frontend partie édition) Side: [OPERATOR] Version target: V0.0.1 Priority: 9 Depends on: OP-005 (StatusPill, InlineBanner, EmptyState, route /campagnes), OP-007 (PATCH + garde backend) Can develop concurrently with: OP-009 Merge order: After OP-007 Estimated complexity: M PRD User Stories: US-OP-04 AC1+AC2+AC3, FR-OP-06 (DOB masqué) Wireframe: docs/wireframes/operator-console/OP-008/ (brief design §11 OP-002)
Objective¶
Livrer la page /campagnes/:id (état BROUILLON) avec : table des contrats (DOB masqué, badge anomalies), drawer latéral d'édition (téléphone/montant/échéance), bouton « Valider et Envoyer » sticky + garde anomalies bloquantes, modale de confirmation envoi. Cette story livre tout sauf le bulk exclude (qui arrive en OP-017).
Backend Scope¶
Consomme :
- GET /api/operator/campaigns/{id} (introduit ici — détail campagne avec contrats)
- GET /api/operator/campaigns/{id}/contracts?cursor=&pageSize=50 (introduit ici)
- PATCH /api/operator/campaigns/{id}/contracts/{ccId} (OP-007)
- POST /api/operator/campaigns/{id}/send (OP-003, garde activée par OP-007)
Service Layer (additif ici)¶
CampaignDetailController.get(...):GET /campaigns/{id}retourne{ id, name, status, type, contractCount, sentAt, createdAt, cohortParams, blockingAnomaliesCount }.CampaignContractsController.list(...):GET /campaigns/{id}/contracts?cursor=&pageSize=50retourne{ data: CampaignContractRow[], nextCursor, hasMore }avecCampaignContractRow = { ccId, contractId, customerName, dobMasked: "**/**/YYYY", phone, productBranch, amount, dueDate, status, blockingAnomaliesCount }.CampaignContractDetailController.get(...):GET /campaigns/{id}/contracts/{ccId}retourne le détail complet pour le drawer, incluantdob: "DD/MM/YYYY"(FR-OP-06 — clair sur fiche individuelle),anomalies: Anomaly[].
API Endpoints (nouveaux ici)¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
GET |
/api/operator/campaigns/{id} |
— | détail campagne | Tous OP |
GET |
/api/operator/campaigns/{id}/contracts?cursor=&pageSize=50 |
— | liste paginée | Tous OP |
GET |
/api/operator/campaigns/{id}/contracts/{ccId} |
— | détail + anomalies | Tous OP |
Validation Rules¶
- pageSize ∈ [1, 100].
Multi-Tenant Considerations¶
- Scope tenant.
- DOB masqué en liste (
**/**/YYYYau niveau API — FR-OP-06).
Audit & Logging¶
- Pas d'audit applicatif sur la lecture. Traçabilité ARTCI couverte par les logs JSON structurés (request log + MDC
tenant_id+user_id+contract_id).CONTRACT_VIEWEDne fait pas partie du catalog 14-types OP-000 (critical rule #3 : AUDIT API rejette lesevent_typeinconnus).
Frontend Scope¶
Pages & Routes¶
- Route
/campagnes/:id(TanStack Router) →CampaignDetailPage.tsxqui sélectionne la vue BROUILLON vs ENVOYÉE parcampaign.status. - Cette story implémente la vue BROUILLON ; OP-009 implémente la vue ENVOYÉE.
Components¶
Sous frontend-operator/src/features/campaigns/detail/:
CampaignDetailDraftPage.tsx— orchestrateur (header + InlineBanner garde + table + drawer + modale).CampaignDraftHeader.tsx— title, status pill, breadcrumb ←, boutons sticky[⋯ Annuler](placeholder désactivé V0.0.1 ; activé OP-016 V0.0.3) +[Valider et Envoyer →].BlockingAnomaliesBanner.tsx— InlineBanner rouge qui apparait siblockingAnomaliesCount > 0avec message FR + lien "Voir les {N} anomalies" (vers /anomalies?campaign={id} — route arrive OP-018, lien inactif V0.0.1).ContractsTable.tsx— DataTable étendu avec colonnes :[selected? — V0.0.1 false], Client, DOB (masqué), Téléphone, Branche, Montant, Échéance, badge anomalies.Drawer.tsx— primitive nouvelle ici (panneau latéral droite 420 px desktop, modal full tablet, focus trap, Esc ferme, restitution focus).ContractEditDrawer.tsx— formulaire React Hook Form + Zod :- Champs éditables :
phone(regex CI),amount(entier > 0),dueDate(DatePicker FR). - Champs lecture seule : référence contrat, branche, DOB en clair (DD/MM/YYYY).
- Liste anomalies du contrat avec lien (résolution arrive OP-018).
- Note locale (textarea) → V0.0.1 : sauvegarde en mémoire React seulement ; sync localStorage et POST arrivent V1 (FR-OP-15).
- Bouton « Enregistrer » → PATCH ; spinner + disable pendant l'appel ; toast success ; refresh row.
ConfirmSendModal.tsx— modale de confirmation envoi (réutiliseModalexistante design system) avec compteur« Envoyer la campagne à {N} client(s) ? ». Sur confirm → POST /send. Si 400 OP_CAMPAIGN_BLOCKING_ANOMALIES → affiche dans la modale« {N} contrat(s) avec anomalies bloquantes. »et redirige vers BlockingAnomaliesBanner.DatePicker.tsx— primitive nouvelle ici (DD/MM/YYYY FR, clavier accessible). Réutilisée par OP-017 (date hors-plateforme).
UI States¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton header + 8 rows | — |
| Empty | Campagne vide (contractCount=0) | « Cette campagne est vide. [Retour à la liste] » |
| Error | InlineBanner + Réessayer | « Erreur de chargement. [Réessayer] » |
| Offline | GlobalBanner + tous les boutons grisés + drawer ouvert mais champs en lecture seule | « Indisponible hors-ligne. » |
| Success | Modifications enregistrées : toast 3s + row mise à jour en fade | « Modifications enregistrées. » / « Campagne envoyée à {n} client(s). » |
Responsive Behavior¶
- ≥ 1280 px : layout complet, drawer 420 px à droite.
- 1024–1279 px : idem.
- 768–1023 px : drawer devient modal centré 90 % largeur.
- < 768 px : page d'accueil mobile (OP-000).
Interactions¶
- Clic sur row OU sur le badge anomalies → ouvre
ContractEditDraweravec focus sur le premier champ éditable (ou sur le champ défectueux si anomalie ciblée). - « Valider et Envoyer » disabled si
blockingAnomaliesCount > 0avec tooltip explicatif. - Drawer : Esc ferme, focus trap, restitution focus sur la row.
- DOB masqué
**/**/YYYYen table ; clairDD/MM/YYYYdans le drawer (FR-OP-06). - Sauvegarde drawer :
Enterdans un champ, ou bouton « Enregistrer ».
Acceptance Criteria¶
AC-001: La table de contrats affiche DOB masqué + badge anomalies (FR-OP-06 + US-OP-04 AC1)
Given une campagne BROUILLON avec 5 contrats dont 2 ont 1 anomalie bloquante
When je charge /campagnes/{id}
Then la table affiche 5 rows avec DOB au format "**/**/YYYY"
And les 2 contrats à anomalie ont un badge rouge "🔴 1" dans la colonne anomalies
And le banner "3 contrats avec anomalies bloquantes" apparait en haut (si compté global).
AC-002: Drawer édition modifie téléphone, montant, échéance uniquement
Given un cabinet_admin clique sur une row
When le drawer ouvre, je modifie le téléphone et le montant, puis je clique "Enregistrer"
Then PATCH est appelé avec les 2 champs
And le toast "Modifications enregistrées." s'affiche
And la row est mise à jour en fade 200ms
And aucun autre champ n'est éditable dans le drawer.
AC-003: Bouton "Valider et Envoyer" disabled si anomalies bloquantes (US-OP-04 AC2)
Given blockingAnomaliesCount > 0
When je regarde le header
Then le bouton "Valider et Envoyer" est grisé avec tooltip "{N} contrat(s) avec anomalies bloquantes. Corrigez ou excluez avant envoi."
And cliquer ne déclenche rien.
AC-004: Modale de confirmation envoi puis transition vers la vue ENVOYÉE (US-OP-04 AC5)
Given 0 anomalie bloquante
When je clique "Valider et Envoyer" puis "Confirmer l'envoi" dans la modale
Then POST /send est appelé
And la page recharge vers /campagnes/{id} en vue ENVOYÉE
And un toast "Campagne envoyée à {N} client(s)." s'affiche.
Compliance Rules¶
- FR-OP-06 : DOB masqué en liste, clair en fiche.
VÉRIFIÉ. - FR-OP-07 : 3 champs éditables uniquement.
VÉRIFIÉ. - FR-OP-02 : garde anomalies bloquantes côté UI.
VÉRIFIÉ.
Standards & Conventions¶
docs/standards/react-typescript-guidelines.mddocs/standards/design-system.md— Drawer, DatePicker tokensdocs/standards/design-principles.md— drawer 420px, focus trap
Testing Requirements¶
Unit Tests¶
DrawerTest: focus trap, Esc ferme, restitution focus.DatePickerTest: format DD/MM/YYYY FR, clavier nav.ContractEditDrawerTest: validation, PATCH appelé, gestion erreur.BlockingAnomaliesBannerTest: visible si count > 0.
Integration Tests¶
- Playwright
edit-contract-flow.spec.tsAC-001..AC-002. send-guard.spec.tsAC-003 + AC-004.
What QA Will Validate¶
- A11y : Tab order, aria-labels, focus visible.
- Rendering 768/1024/1280/1440.
- DOB jamais clair en table (audit visuel + grep).
Out of Scope¶
- Bulk exclude (action bar bottom) → OP-017 (V0.0.3).
- Annulation campagne → OP-016 (V0.0.3).
- Notes synchronisées localStorage → V1 (FR-OP-15).
- Vue ENVOYÉE → OP-009.
Definition of Done¶
- [ ] Route
/campagnes/:idrend la vue BROUILLON - [ ] AC-001..AC-004 passent (Playwright)
- [ ] DOB masqué en table, clair en drawer (FR-OP-06 vérifié)
- [ ] Drawer + DatePicker primitives créées et exportables
- [ ] Bouton "Valider et Envoyer" sticky avec garde anomalies
- [ ] Modale de confirmation conforme design §4.6
- [ ] PATCH côté UI gère succès + erreur (toast + InlineBanner)
- [ ] 5 états UI implémentés
- [ ] Rendu 768/1024/1280/1440
- [ ] Tenant isolation conservée
- [ ] Tests verts