Aller au contenu

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=50 retourne { data: CampaignContractRow[], nextCursor, hasMore } avec CampaignContractRow = { 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, incluant dob: "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 (**/**/YYYY au 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_VIEWED ne fait pas partie du catalog 14-types OP-000 (critical rule #3 : AUDIT API rejette les event_type inconnus).

Frontend Scope

Pages & Routes

  • Route /campagnes/:id (TanStack Router) → CampaignDetailPage.tsx qui sélectionne la vue BROUILLON vs ENVOYÉE par campaign.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 si blockingAnomaliesCount > 0 avec 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éutilise Modal existante 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 ContractEditDrawer avec focus sur le premier champ éditable (ou sur le champ défectueux si anomalie ciblée).
  • « Valider et Envoyer » disabled si blockingAnomaliesCount > 0 avec tooltip explicatif.
  • Drawer : Esc ferme, focus trap, restitution focus sur la row.
  • DOB masqué **/**/YYYY en table ; clair DD/MM/YYYY dans le drawer (FR-OP-06).
  • Sauvegarde drawer : Enter dans 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.md
  • docs/standards/design-system.md — Drawer, DatePicker tokens
  • docs/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.ts AC-001..AC-002.
  • send-guard.spec.ts AC-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/:id rend 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