Formulaire de contact : remontée de bugs / suggestions par les utilisateurs, consultable côté admin #80

Closed
opened 2026-09-07 00:17:49 +02:00 by kyuno · 0 comments
Owner

Résumé

Donner aux utilisateurs connectés un moyen de remonter un bug ou une
suggestion
depuis l'app, via un formulaire simple. Chaque message est
stocké en base et consultable côté admin (liste en lecture seule —
pas de workflow de traitement pour cette première version).

Rien d'équivalent aujourd'hui : aucun concept « contact » / « feedback » /
« signalement » dans le code (vérifié : apps/api, apps/web,
packages/shared, prisma/schema.prisma, specs/). Les précédents les
plus proches sont le triage des corrections tech-step
(StepTechStepCorrection + page /admin/corrections) et la revue des
ingrédients hors-catalogue (Ingredient.reviewedAt + /admin/catalogue) :
contenu soumis par un utilisateur, consulté par un admin.

Décisions de conception (validées avec l'utilisateur)

Question Décision Pourquoi
Qui peut envoyer ? Utilisateurs connectés uniquement Route requireAuth, message rattaché au UserProfile. Pas de champ nom/email à saisir, pas d'anti-spam à prévoir.
Champs du formulaire Type (Bug / Suggestion / Autre) + message libre + contexte technique capturé automatiquement (URL courante, user-agent, version de l'app) Le contexte auto est invisible pour l'utilisateur mais indispensable pour reproduire un bug. Le type structure le tri côté admin.
Côté admin Liste en lecture seule (récents d'abord, filtre par type) Pas de statut nouveau/en cours/traité ni de note admin pour cette version — additif plus tard si besoin (un champ + une migration).
Priorité Medium Utile mais non bloquant.

Modèle de données

Nouveau modèle Prisma ContactMessage (apps/api/prisma/schema.prisma,
nouvelle section « Contact » après la section « Admin application ») :

/// Un message envoyé via le formulaire de contact de l'app (bug,
/// suggestion, autre). Écrit par un utilisateur connecté, jamais modifié
/// ensuite — consulté en lecture seule côté admin (`/admin/messages`).
/// `authorId` est nullable + `onDelete: SetNull` (même choix que
/// `Ingredient.createdById`) : un signalement de bug garde son intérêt même
/// si le compte qui l'a envoyé est supprimé entre-temps.
model ContactMessage {
  id         Int      @id @default(autoincrement())
  authorId   Int?     @map("author_id")
  /// "bug" | "suggestion" | "other" — String + const partagée
  /// (`CONTACT_MESSAGE_TYPES`), même approche que
  /// `TechStepTrainingSuggestion.status` : ajouter une valeur = une ligne,
  /// pas de migration d'enum Postgres.
  type       String
  message    String
  /// Contexte capturé automatiquement à l'envoi, pour le triage — tous
  /// nullables (un client ancien ou une requête directe peut les omettre).
  pageUrl    String?  @map("page_url")
  userAgent  String?  @map("user_agent")
  appVersion String?  @map("app_version")
  createdAt  DateTime @default(now()) @map("created_at")

  author UserProfile? @relation(fields: [authorId], references: [id], onDelete: SetNull)

  @@index([createdAt])
  @@map("contact_message")
}
  • relation inverse contactMessages ContactMessage[] sur UserProfile.
    Migration prisma migrate dev --name contact_message — nouvelle table
    uniquement, aucun backfill.

Backend

Envoi (utilisateur) — nouveau module apps/api/src/modules/contact/ :

  • contact.routes.ts — POST /contact, requireAuth, monté /contact
    dans app.ts.
  • contact.service.ts — submitContactMessage(authorId, input, userAgent)
    insère la ligne. userAgent est lu de req.headers["user-agent"] côté
    route (jamais fait confiance au client pour ça) ; pageUrl / appVersion
    viennent du body ; type validé contre CONTACT_MESSAGE_TYPES.
  • Optionnel (recommandé, quasi gratuit) :
    analytics.recordEvent("contact.submitted", { actorType: "user", actorId: authorId, context: { type } })
    — admin-metrics.service.ts agrège déjà les AnalyticsEvent par type
    de façon générique, le message apparaîtra dans les séries du dashboard
    sans code en plus.

Consultation (admin) — apps/api/src/modules/admin/admin-contact.{routes,service}.ts :

  • GET /admin/api/contact?type= — requireAdmin, tri createdAt desc,
    plafond de page (CONTACTS_PAGE_SIZE = 200, même esprit que
    CORRECTIONS_PAGE_SIZE). Filtre type optionnel.
  • listContactMessages(query) renvoie ContactMessageAdminView[] : id,
    type, message, pageUrl, userAgent, appVersion, createdAt
    (ISO), et author: { id, firstName, lastName, email } | null (null =
    compte supprimé).
  • Brancher dans admin.routes.ts : adminRouter.use("/contact", adminContactRouter).

Contrat partagé (packages/shared/src, + barrel index.ts) :

  • schemas/contact.ts — CONTACT_MESSAGE_TYPES = ["bug", "suggestion", "other"] as const,
    submitContactMessageSchema (type enum, message
    z.string().trim().min(10).max(5000), pageUrl z.string().max(500).optional(),
    appVersion z.string().max(50).optional()), listContactMessagesQuerySchema
    (type optionnel).
  • types/contact.ts — ContactMessageAdminView, ContactMessageType.
  • VALIDATION_ERROR suffit a priori, pas de nouveau ErrorCode.

Frontend

Formulaire utilisateur :

  • Page apps/web/src/pages/settings/ContactPage.tsx (+ .scss, + un
    contact.ts pour toute logique pure). Route /parametres/contact sous
    RequireAuth / AppLayout dans App.tsx.
  • Entrée de nav dans AppLayout.tsx SETTINGS_NAV_ITEMS
    ({ to: "/parametres/contact", key: "contact", Icon: … }) + clé
    layout.settings.nav.contact.
  • apiClient.submitContactMessage(input) dans api/client.ts — envoie
    type, message, pageUrl: window.location.pathname,
    appVersion: __APP_VERSION__ (global déjà défini par vite.config.ts).
  • Formulaire : sélecteur de type, textarea message, validation via
    submitContactMessageSchema, états succès / erreur (i18n, jamais
    err.message brut — cf. ErrorMessageService).

Vue admin :

  • Page apps/web/src/pages/admin/messages/MessagesPage.tsx (+ .scss).
    Route messages dans le groupe /admin de App.tsx.
  • Entrée de nav dans AdminLayout.tsx NAV_ITEMS
    ({ to: "/admin/messages", key: "messages", Icon: <lucide, ex. Inbox>, end: false })
    • clé admin.nav.messages.
  • adminClient.listContactMessages({ type }) dans api/admin-client.ts.
  • Tableau lecture seule : date, expéditeur (nom + email, ou « compte
    supprimé »), badge type, message, URL de page, user-agent (tronqué).
    Filtre par type. Aucune action.

Tests (obligatoires par PR, cf. specs/dev-conventions.md)

  • API (Mocha + Chai, apps/api/test/) :
    • contact.service : insère avec les bons champs ; userAgent pris de
      l'en-tête ; rejette message trop court / trop long / type inconnu ;
      authorId bien rattaché.
    • admin-contact.service : tri récent d'abord ; filtre type ; plafond
      de page ; author: null quand le UserProfile a été supprimé.
  • Web (Cypress) :
    • e2e Gherkin contact.feature + steps : « En tant qu'utilisateur je veux
      signaler un bug » — remplir, envoyer, voir la confirmation (mock
      POST /contact, asserter que le body inclut pageUrl / appVersion).
    • spec layout admin-messages.cy.ts : la page admin liste les messages
      mockés, le filtre par type marche (mock GET /admin/api/contact).
    • spec layout pour ContactPage (rendu + garde RequireAuth).

Documentation à mettre à jour

  • specs/batch-cooking-modele.md — ajouter le modèle ContactMessage.
  • specs/backend-architecture.md — court paragraphe sur le module
    contact, ligne GET /admin/api/contact dans le tableau des endpoints
    admin, mention de l'event contact.submitted si retenu.
  • specs/frontend-architecture.md — si la liste des pages y est tenue,
    ajouter /parametres/contact et /admin/messages.

Découpage proposé

Deux PR courtes et stackées (cf. « PRs plutôt petites et scopées ») :

  1. feat(contact): modèle + POST /contact + formulaire utilisateur —
    modèle Prisma + migration, contrat partagé, module contact API, page
    /parametres/contact + nav + i18n, tests API contact.service +
    Cypress e2e.
  2. feat(admin): consultation des messages de contact —
    GET /admin/api/contact + service, page /admin/messages + nav + i18n,
    tests admin-contact.service + Cypress layout (event analytics ici si
    retenu).

Hors scope

  • Envoi par un visiteur non connecté (formulaire sur login/landing,
    champs nom/email, rate-limit anti-spam) — décidé : connectés seulement.
  • Workflow de traitement admin : statut (nouveau / en cours / traité /
    rejeté), note interne, assignation — la vue admin est en lecture seule
    pour cette version. Ajout additif plus tard.
  • Notification à l'admin (email, push socket.io « monitoring ») à la
    réception, et réponse à l'utilisateur (pas d'infra email dans le repo).
  • Pièces jointes / captures d'écran — nécessiterait un sous-système de
    stockage de fichiers (les images du repo sont des URLs externes, aucun
    upload aujourd'hui).
  • Traduction du contenu saisi par l'utilisateur (le message reste tel quel).

🤖 Filed by Claude Code

## Résumé Donner aux **utilisateurs connectés** un moyen de **remonter un bug ou une suggestion** depuis l'app, via un formulaire simple. Chaque message est **stocké en base** et **consultable côté admin** (liste en lecture seule — pas de workflow de traitement pour cette première version). Rien d'équivalent aujourd'hui : aucun concept « contact » / « feedback » / « signalement » dans le code (vérifié : `apps/api`, `apps/web`, `packages/shared`, `prisma/schema.prisma`, `specs/`). Les précédents les plus proches sont le triage des corrections tech-step (`StepTechStepCorrection` + page `/admin/corrections`) et la revue des ingrédients hors-catalogue (`Ingredient.reviewedAt` + `/admin/catalogue`) : contenu soumis par un utilisateur, consulté par un admin. ## Décisions de conception (validées avec l'utilisateur) | Question | Décision | Pourquoi | |---|---|---| | Qui peut envoyer ? | **Utilisateurs connectés uniquement** | Route `requireAuth`, message rattaché au `UserProfile`. Pas de champ nom/email à saisir, pas d'anti-spam à prévoir. | | Champs du formulaire | **Type** (Bug / Suggestion / Autre) + **message** libre + **contexte technique capturé automatiquement** (URL courante, user-agent, version de l'app) | Le contexte auto est invisible pour l'utilisateur mais indispensable pour reproduire un bug. Le `type` structure le tri côté admin. | | Côté admin | **Liste en lecture seule** (récents d'abord, filtre par type) | Pas de statut nouveau/en cours/traité ni de note admin pour cette version — additif plus tard si besoin (un champ + une migration). | | Priorité | **Medium** | Utile mais non bloquant. | ## Modèle de données Nouveau modèle Prisma `ContactMessage` (`apps/api/prisma/schema.prisma`, nouvelle section « Contact » après la section « Admin application ») : ```prisma /// Un message envoyé via le formulaire de contact de l'app (bug, /// suggestion, autre). Écrit par un utilisateur connecté, jamais modifié /// ensuite — consulté en lecture seule côté admin (`/admin/messages`). /// `authorId` est nullable + `onDelete: SetNull` (même choix que /// `Ingredient.createdById`) : un signalement de bug garde son intérêt même /// si le compte qui l'a envoyé est supprimé entre-temps. model ContactMessage { id Int @id @default(autoincrement()) authorId Int? @map("author_id") /// "bug" | "suggestion" | "other" — String + const partagée /// (`CONTACT_MESSAGE_TYPES`), même approche que /// `TechStepTrainingSuggestion.status` : ajouter une valeur = une ligne, /// pas de migration d'enum Postgres. type String message String /// Contexte capturé automatiquement à l'envoi, pour le triage — tous /// nullables (un client ancien ou une requête directe peut les omettre). pageUrl String? @map("page_url") userAgent String? @map("user_agent") appVersion String? @map("app_version") createdAt DateTime @default(now()) @map("created_at") author UserProfile? @relation(fields: [authorId], references: [id], onDelete: SetNull) @@index([createdAt]) @@map("contact_message") } ``` + relation inverse `contactMessages ContactMessage[]` sur `UserProfile`. Migration `prisma migrate dev --name contact_message` — nouvelle table uniquement, aucun backfill. ## Backend **Envoi (utilisateur)** — nouveau module `apps/api/src/modules/contact/` : - `contact.routes.ts` — `POST /contact`, `requireAuth`, monté `/contact` dans `app.ts`. - `contact.service.ts` — `submitContactMessage(authorId, input, userAgent)` insère la ligne. `userAgent` est lu de `req.headers["user-agent"]` côté route (jamais fait confiance au client pour ça) ; `pageUrl` / `appVersion` viennent du body ; `type` validé contre `CONTACT_MESSAGE_TYPES`. - Optionnel (recommandé, quasi gratuit) : `analytics.recordEvent("contact.submitted", { actorType: "user", actorId: authorId, context: { type } })` — `admin-metrics.service.ts` agrège déjà les `AnalyticsEvent` par `type` de façon générique, le message apparaîtra dans les séries du dashboard sans code en plus. **Consultation (admin)** — `apps/api/src/modules/admin/admin-contact.{routes,service}.ts` : - `GET /admin/api/contact?type=` — `requireAdmin`, tri `createdAt desc`, plafond de page (`CONTACTS_PAGE_SIZE = 200`, même esprit que `CORRECTIONS_PAGE_SIZE`). Filtre `type` optionnel. - `listContactMessages(query)` renvoie `ContactMessageAdminView[]` : `id`, `type`, `message`, `pageUrl`, `userAgent`, `appVersion`, `createdAt` (ISO), et `author: { id, firstName, lastName, email } | null` (null = compte supprimé). - Brancher dans `admin.routes.ts` : `adminRouter.use("/contact", adminContactRouter)`. **Contrat partagé** (`packages/shared/src`, + barrel `index.ts`) : - `schemas/contact.ts` — `CONTACT_MESSAGE_TYPES = ["bug", "suggestion", "other"] as const`, `submitContactMessageSchema` (`type` enum, `message` `z.string().trim().min(10).max(5000)`, `pageUrl` `z.string().max(500).optional()`, `appVersion` `z.string().max(50).optional()`), `listContactMessagesQuerySchema` (`type` optionnel). - `types/contact.ts` — `ContactMessageAdminView`, `ContactMessageType`. - `VALIDATION_ERROR` suffit a priori, pas de nouveau `ErrorCode`. ## Frontend **Formulaire utilisateur** : - Page `apps/web/src/pages/settings/ContactPage.tsx` (+ `.scss`, + un `contact.ts` pour toute logique pure). Route `/parametres/contact` sous `RequireAuth` / `AppLayout` dans `App.tsx`. - Entrée de nav dans `AppLayout.tsx` `SETTINGS_NAV_ITEMS` (`{ to: "/parametres/contact", key: "contact", Icon: … }`) + clé `layout.settings.nav.contact`. - `apiClient.submitContactMessage(input)` dans `api/client.ts` — envoie `type`, `message`, `pageUrl: window.location.pathname`, `appVersion: __APP_VERSION__` (global déjà défini par `vite.config.ts`). - Formulaire : sélecteur de type, textarea message, validation via `submitContactMessageSchema`, états succès / erreur (i18n, jamais `err.message` brut — cf. `ErrorMessageService`). **Vue admin** : - Page `apps/web/src/pages/admin/messages/MessagesPage.tsx` (+ `.scss`). Route `messages` dans le groupe `/admin` de `App.tsx`. - Entrée de nav dans `AdminLayout.tsx` `NAV_ITEMS` (`{ to: "/admin/messages", key: "messages", Icon: <lucide, ex. Inbox>, end: false }`) + clé `admin.nav.messages`. - `adminClient.listContactMessages({ type })` dans `api/admin-client.ts`. - Tableau lecture seule : date, expéditeur (nom + email, ou « compte supprimé »), badge type, message, URL de page, user-agent (tronqué). Filtre par type. Aucune action. ## Tests (obligatoires par PR, cf. specs/dev-conventions.md) - **API (Mocha + Chai, `apps/api/test/`)** : - `contact.service` : insère avec les bons champs ; `userAgent` pris de l'en-tête ; rejette message trop court / trop long / `type` inconnu ; `authorId` bien rattaché. - `admin-contact.service` : tri récent d'abord ; filtre `type` ; plafond de page ; `author: null` quand le `UserProfile` a été supprimé. - **Web (Cypress)** : - e2e Gherkin `contact.feature` + steps : « En tant qu'utilisateur je veux signaler un bug » — remplir, envoyer, voir la confirmation (mock `POST /contact`, asserter que le body inclut `pageUrl` / `appVersion`). - spec layout `admin-messages.cy.ts` : la page admin liste les messages mockés, le filtre par type marche (mock `GET /admin/api/contact`). - spec layout pour `ContactPage` (rendu + garde `RequireAuth`). ## Documentation à mettre à jour - `specs/batch-cooking-modele.md` — ajouter le modèle `ContactMessage`. - `specs/backend-architecture.md` — court paragraphe sur le module `contact`, ligne `GET /admin/api/contact` dans le tableau des endpoints admin, mention de l'event `contact.submitted` si retenu. - `specs/frontend-architecture.md` — si la liste des pages y est tenue, ajouter `/parametres/contact` et `/admin/messages`. ## Découpage proposé Deux PR courtes et stackées (cf. « PRs plutôt petites et scopées ») : 1. **`feat(contact): modèle + POST /contact + formulaire utilisateur`** — modèle Prisma + migration, contrat partagé, module `contact` API, page `/parametres/contact` + nav + i18n, tests API `contact.service` + Cypress e2e. 2. **`feat(admin): consultation des messages de contact`** — `GET /admin/api/contact` + service, page `/admin/messages` + nav + i18n, tests `admin-contact.service` + Cypress layout (event analytics ici si retenu). ## Hors scope - **Envoi par un visiteur non connecté** (formulaire sur login/landing, champs nom/email, rate-limit anti-spam) — décidé : connectés seulement. - **Workflow de traitement admin** : statut (nouveau / en cours / traité / rejeté), note interne, assignation — la vue admin est en lecture seule pour cette version. Ajout additif plus tard. - **Notification** à l'admin (email, push socket.io « monitoring ») à la réception, et **réponse** à l'utilisateur (pas d'infra email dans le repo). - **Pièces jointes / captures d'écran** — nécessiterait un sous-système de stockage de fichiers (les images du repo sont des URLs externes, aucun upload aujourd'hui). - **Traduction** du contenu saisi par l'utilisateur (le message reste tel quel). --- 🤖 Filed by [Claude Code](https://claude.com/claude-code)
kyuno added the
enhancement
planned
Medium
labels 2026-09-07 00:17:49 +02:00
kyuno closed this issue 2026-09-08 09:17:21 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: kyuno/batchCooking#80
No description provided.