Formulaire de contact : remontée de bugs / suggestions par les utilisateurs, consultable côté admin #80
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 lesplus proches sont le triage des corrections tech-step
(
StepTechStepCorrection+ page/admin/corrections) et la revue desingré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)
requireAuth, message rattaché auUserProfile. Pas de champ nom/email à saisir, pas d'anti-spam à prévoir.typestructure le tri côté admin.Modèle de données
Nouveau modèle Prisma
ContactMessage(apps/api/prisma/schema.prisma,nouvelle section « Contact » après la section « Admin application ») :
contactMessages ContactMessage[]surUserProfile.Migration
prisma migrate dev --name contact_message— nouvelle tableuniquement, aucun backfill.
Backend
Envoi (utilisateur) — nouveau module
apps/api/src/modules/contact/:contact.routes.ts—POST /contact,requireAuth, monté/contactdans
app.ts.contact.service.ts—submitContactMessage(authorId, input, userAgent)insère la ligne.
userAgentest lu dereq.headers["user-agent"]côtéroute (jamais fait confiance au client pour ça) ;
pageUrl/appVersionviennent du body ;
typevalidé contreCONTACT_MESSAGE_TYPES.analytics.recordEvent("contact.submitted", { actorType: "user", actorId: authorId, context: { type } })—
admin-metrics.service.tsagrège déjà lesAnalyticsEventpartypede 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, tricreatedAt desc,plafond de page (
CONTACTS_PAGE_SIZE = 200, même esprit queCORRECTIONS_PAGE_SIZE). Filtretypeoptionnel.listContactMessages(query)renvoieContactMessageAdminView[]:id,type,message,pageUrl,userAgent,appVersion,createdAt(ISO), et
author: { id, firstName, lastName, email } | null(null =compte supprimé).
admin.routes.ts:adminRouter.use("/contact", adminContactRouter).Contrat partagé (
packages/shared/src, + barrelindex.ts) :schemas/contact.ts—CONTACT_MESSAGE_TYPES = ["bug", "suggestion", "other"] as const,submitContactMessageSchema(typeenum,messagez.string().trim().min(10).max(5000),pageUrlz.string().max(500).optional(),appVersionz.string().max(50).optional()),listContactMessagesQuerySchema(
typeoptionnel).types/contact.ts—ContactMessageAdminView,ContactMessageType.VALIDATION_ERRORsuffit a priori, pas de nouveauErrorCode.Frontend
Formulaire utilisateur :
apps/web/src/pages/settings/ContactPage.tsx(+.scss, + uncontact.tspour toute logique pure). Route/parametres/contactsousRequireAuth/AppLayoutdansApp.tsx.AppLayout.tsxSETTINGS_NAV_ITEMS(
{ to: "/parametres/contact", key: "contact", Icon: … }) + clélayout.settings.nav.contact.apiClient.submitContactMessage(input)dansapi/client.ts— envoietype,message,pageUrl: window.location.pathname,appVersion: __APP_VERSION__(global déjà défini parvite.config.ts).submitContactMessageSchema, états succès / erreur (i18n, jamaiserr.messagebrut — cf.ErrorMessageService).Vue admin :
apps/web/src/pages/admin/messages/MessagesPage.tsx(+.scss).Route
messagesdans le groupe/admindeApp.tsx.AdminLayout.tsxNAV_ITEMS(
{ to: "/admin/messages", key: "messages", Icon: <lucide, ex. Inbox>, end: false })admin.nav.messages.adminClient.listContactMessages({ type })dansapi/admin-client.ts.supprimé »), badge type, message, URL de page, user-agent (tronqué).
Filtre par type. Aucune action.
Tests (obligatoires par PR, cf. specs/dev-conventions.md)
apps/api/test/) :contact.service: insère avec les bons champs ;userAgentpris del'en-tête ; rejette message trop court / trop long /
typeinconnu ;authorIdbien rattaché.admin-contact.service: tri récent d'abord ; filtretype; plafondde page ;
author: nullquand leUserProfilea été supprimé.contact.feature+ steps : « En tant qu'utilisateur je veuxsignaler un bug » — remplir, envoyer, voir la confirmation (mock
POST /contact, asserter que le body inclutpageUrl/appVersion).admin-messages.cy.ts: la page admin liste les messagesmockés, le filtre par type marche (mock
GET /admin/api/contact).ContactPage(rendu + gardeRequireAuth).Documentation à mettre à jour
specs/batch-cooking-modele.md— ajouter le modèleContactMessage.specs/backend-architecture.md— court paragraphe sur le modulecontact, ligneGET /admin/api/contactdans le tableau des endpointsadmin, mention de l'event
contact.submittedsi retenu.specs/frontend-architecture.md— si la liste des pages y est tenue,ajouter
/parametres/contactet/admin/messages.Découpage proposé
Deux PR courtes et stackées (cf. « PRs plutôt petites et scopées ») :
feat(contact): modèle + POST /contact + formulaire utilisateur—modèle Prisma + migration, contrat partagé, module
contactAPI, page/parametres/contact+ nav + i18n, tests APIcontact.service+Cypress e2e.
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 siretenu).
Hors scope
champs nom/email, rate-limit anti-spam) — décidé : connectés seulement.
rejeté), note interne, assignation — la vue admin est en lecture seule
pour cette version. Ajout additif plus tard.
réception, et réponse à l'utilisateur (pas d'infra email dans le repo).
stockage de fichiers (les images du repo sont des URLs externes, aucun
upload aujourd'hui).
🤖 Filed by Claude Code