feat(contact): modèle + POST /contact + formulaire utilisateur (#80) #123

Merged
kyuno merged 2 commits from feat/contact-message-model-and-form into main 2026-09-08 09:17:20 +02:00
Owner

PR 1/2 de l'issue #80 — le formulaire in-app de remontée de bug / suggestion pour les utilisateurs connectés. La consultation admin (GET /admin/api/contact + page /admin/messages) suivra en PR 2, stackée sur celle-ci.

Contenu

Modèle & contrat

  • ContactMessage (Prisma) + relation inverse contactMessages sur UserProfile ; migration 20260908120000_contact_message (table neuve, aucun backfill). authorId nullable + onDelete: SetNull — un signalement survit à la suppression de son compte (même choix que Ingredient.createdById).
  • packages/shared/src/schemas/contact.ts : CONTACT_MESSAGE_TYPES (bug | suggestion | other), submitContactMessageSchema (type enum, message 10–5000 trim, pageUrl / appVersion optionnels). Le user-agent n'est pas dans le schéma — le serveur le lit de l'en-tête.
  • "contact_message" ajouté à la liste TRUNCATE de reset-db.ts.

API — module contact

  • POST /contact (requireAuth, monté /contact) → 204 sans corps. 204 et non 201-avec-corps : le formulaire n'a rien à faire de la ligne stockée, et ApiClient._request ne traite que 204 comme sans corps (même forme que POST /auth/logout).
  • submitContactMessage(authorId, input, userAgent) insère la ligne puis émet analytics.recordEvent("contact.submitted", { actorType: "user", actorId, context: { type } }) (fire-and-forget) — admin-metrics agrège les AnalyticsEvent par type génériquement, l'event apparaît dans le dashboard sans code en plus.

Front — page /parametres/contact

  • ContactPage (+ contact.ts pour buildContactMessageInput, + .scss) : <select> de type, <textarea> message, validation cliente via submitContactMessageSchema (messages du schéma affichés tels quels), états succès / erreur via ErrorMessageService (jamais err.message).
  • Entrée de nav « Nous contacter » dans SETTINGS_ITEMS + clé i18n layout.settings.nav.contact + bloc contact.* + icône ContactIcon (MessageSquare).
  • apiClient.submitContactMessage(input) — ajoute pageUrl: window.location.pathname et appVersion: __APP_VERSION__.
  • Route sous RequireAuth / AppLayout dans App.tsx.

Tests

  • apps/api/test/contact.test.ts (Mocha) : 401 sans session ; insertion avec les bons champs + authorId = appelant + user-agent pris de l'en-tête ; user-agent du corps ignoré ; pageUrl / appVersion null si omis ; message trop court / trop long / type inconnu → 400 VALIDATION_ERROR.
  • apps/web/cypress/e2e/contact.feature + contact.ts : envoi d'un bug → confirmation, le corps de la requête porte pageUrl / appVersion ; message trop court rejeté côté client sans requête.
  • apps/web/cypress/e2e/contact-page.cy.ts : garde RequireAuth (→ /login), rendu du formulaire, label d'erreur traduit (jamais le message serveur brut) sur échec.

Docs : batch-cooking-modele.md, backend-architecture.md, frontend-architecture.md.

Vérifié localement

pnpm lint ✅ · pnpm build (tsc api + web, vite build) ✅ · Cypress contact.feature + contact-page.cy.ts + specs voisines (sidebar, layout, app-settings-page, user-preferences) : 24/24 ✅. Mocha non exécutable ici (pas de Postgres local) → CI.

Closes #80 partiellement (PR 1/2).

🤖 Generated with Claude Code

PR 1/2 de l'issue #80 — le formulaire in-app de remontée de bug / suggestion pour les utilisateurs connectés. La consultation admin (`GET /admin/api/contact` + page `/admin/messages`) suivra en PR 2, stackée sur celle-ci. ## Contenu **Modèle & contrat** - `ContactMessage` (Prisma) + relation inverse `contactMessages` sur `UserProfile` ; migration `20260908120000_contact_message` (table neuve, aucun backfill). `authorId` nullable + `onDelete: SetNull` — un signalement survit à la suppression de son compte (même choix que `Ingredient.createdById`). - `packages/shared/src/schemas/contact.ts` : `CONTACT_MESSAGE_TYPES` (`bug` | `suggestion` | `other`), `submitContactMessageSchema` (`type` enum, `message` 10–5000 `trim`, `pageUrl` / `appVersion` optionnels). Le user-agent **n'est pas** dans le schéma — le serveur le lit de l'en-tête. - `"contact_message"` ajouté à la liste TRUNCATE de `reset-db.ts`. **API — module `contact`** - `POST /contact` (`requireAuth`, monté `/contact`) → `204` sans corps. `204` et non `201`-avec-corps : le formulaire n'a rien à faire de la ligne stockée, et `ApiClient._request` ne traite que `204` comme sans corps (même forme que `POST /auth/logout`). - `submitContactMessage(authorId, input, userAgent)` insère la ligne puis émet `analytics.recordEvent("contact.submitted", { actorType: "user", actorId, context: { type } })` (fire-and-forget) — `admin-metrics` agrège les `AnalyticsEvent` par `type` génériquement, l'event apparaît dans le dashboard sans code en plus. **Front — page `/parametres/contact`** - `ContactPage` (+ `contact.ts` pour `buildContactMessageInput`, + `.scss`) : `<select>` de type, `<textarea>` message, validation cliente via `submitContactMessageSchema` (messages du schéma affichés tels quels), états succès / erreur via `ErrorMessageService` (jamais `err.message`). - Entrée de nav « Nous contacter » dans `SETTINGS_ITEMS` + clé i18n `layout.settings.nav.contact` + bloc `contact.*` + icône `ContactIcon` (`MessageSquare`). - `apiClient.submitContactMessage(input)` — ajoute `pageUrl: window.location.pathname` et `appVersion: __APP_VERSION__`. - Route sous `RequireAuth` / `AppLayout` dans `App.tsx`. **Tests** - `apps/api/test/contact.test.ts` (Mocha) : 401 sans session ; insertion avec les bons champs + `authorId` = appelant + user-agent pris de l'en-tête ; user-agent du corps ignoré ; `pageUrl` / `appVersion` `null` si omis ; message trop court / trop long / `type` inconnu → `400 VALIDATION_ERROR`. - `apps/web/cypress/e2e/contact.feature` + `contact.ts` : envoi d'un bug → confirmation, le corps de la requête porte `pageUrl` / `appVersion` ; message trop court rejeté côté client sans requête. - `apps/web/cypress/e2e/contact-page.cy.ts` : garde `RequireAuth` (→ `/login`), rendu du formulaire, label d'erreur traduit (jamais le message serveur brut) sur échec. **Docs** : `batch-cooking-modele.md`, `backend-architecture.md`, `frontend-architecture.md`. ## Vérifié localement `pnpm lint` ✅ · `pnpm build` (tsc api + web, vite build) ✅ · Cypress `contact.feature` + `contact-page.cy.ts` + specs voisines (`sidebar`, `layout`, `app-settings-page`, `user-preferences`) : 24/24 ✅. Mocha non exécutable ici (pas de Postgres local) → CI. Closes #80 partiellement (PR 1/2). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
kyuno added 1 commit 2026-09-08 08:27:17 +02:00
feat(contact): modèle + POST /contact + formulaire utilisateur (#80)
Some checks failed
CI / build (push) Successful in 2m22s
CI / lint (push) Successful in 2m26s
CI / intent-service-test (push) Successful in 4m8s
CI / e2e (push) Successful in 12m40s
CI / test (push) Failing after 15m4s
CI / build-android (push) Has been skipped
a252640fe6
Formulaire in-app de remontée de bug / suggestion pour les utilisateurs
connectés (PR 1/2 de l'issue #80).

- Modèle Prisma `ContactMessage` (+ relation inverse sur `UserProfile`) et
  migration `20260908120000_contact_message` — table neuve, aucun backfill.
  `authorId` nullable + `onDelete: SetNull` : un signalement survit à la
  suppression de son compte.
- Contrat partagé `schemas/contact.ts` : `CONTACT_MESSAGE_TYPES`,
  `submitContactMessageSchema` (type enum, message 10-5000 trim, pageUrl /
  appVersion optionnels — jamais le user-agent).
- Module API `contact` : `POST /contact` (`requireAuth`, monté `/contact`),
  `204` sans corps. Le user-agent est lu de l'en-tête de requête, jamais du
  corps. Émet `analytics.recordEvent(contact.submitted, …)`.
- Page `/parametres/contact` (`ContactPage` + `contact.ts` + `.scss`) :
  select de type, textarea, validation cliente via le schéma partagé, états
  succès / erreur (`ErrorMessageService`, jamais `err.message`). Entrée de
  nav « Nous contacter » dans `SETTINGS_ITEMS` + clé i18n + icône.
  `apiClient.submitContactMessage(input)` ajoute `pageUrl` / `appVersion`.
- Tests : `apps/api/test/contact.test.ts` (Mocha — insertion, en-tête UA,
  champs nullables, message trop court / trop long, type inconnu),
  `contact.feature` + steps (Cypress e2e — envoi + confirmation + contexte),
  `contact-page.cy.ts` (rendu + garde `RequireAuth` + label d'erreur).
- Docs : `batch-cooking-modele.md`, `backend-architecture.md`,
  `frontend-architecture.md`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kyuno referenced this pull request from a commit 2026-09-08 08:57:50 +02:00
kyuno added 1 commit 2026-09-08 08:57:50 +02:00
chore: relance la CI (#123)
All checks were successful
CI / lint (push) Successful in 3m40s
CI / build (push) Successful in 3m43s
CI / intent-service-test (push) Successful in 4m35s
CI / e2e (push) Successful in 9m39s
CI / test (push) Successful in 18m14s
CI / build-android (push) Has been skipped
b873b70946
Le job `test` a échoué sur `a252640` par contention Postgres avec la CI de
#124 lancée en parallèle (15 timeouts de hook « before each » + quelques
PrismaClientKnownRequestError, tous hors périmètre — les 7 tests `Contact`
passent). #124 (surensemble de cette branche) est vert. Relance isolée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kyuno merged commit 9b6cee9950 into main 2026-09-08 09:17:20 +02:00
Sign in to join this conversation.
No reviewers
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#123
No description provided.