Itchek · Lot suivant · Plan du 17 juillet 2026
Avenant mobile — les trois demandes du call qui bloquent Achille
Réunion du 16/07 : des demandes admin dont la vraie finalité est de débloquer les écrans mobiles. L'icône de forfait est déjà implémentée (17/07) ; restent deux conceptions jumelles et additives — le lien forfait → produits et la catégorisation des groupes — construites sur le même pattern, sans migration.
État des lieux — vérifié dans le code, pas supposé
| Fonctionnalité | Modèle Mongo | API backend | Admin UI | Mobile (Achille) |
|---|---|---|---|---|
| A · Flag marque/modèle forfait → sélection produit |
à faire lien absent d' item.js |
à faire validation Joi à étendre |
à faire sélecteur dans Tarifs |
bloqué attend le contrat |
| B · Icône par forfait | déjà faititem.icons[] → ref Icon |
fait (17/07) Joi + controller acceptent icons |
fait (17/07) picker livré dans le dialogue d'option |
à faire affichage (contrat §3) |
| C · Catégorisation des groupes & options « catégories principales » WC / Lavabo / Évier |
à faire niveau catégorie absent |
à faire | à faire | bloqué attend le contrat |
L'expiration des documents (évoquée en réunion) est hors périmètre de ce lot : l'admin est déjà livré, le reste est du travail mobile pur. B est désormais implémentée de bout en bout côté admin — il reste A (le lien produit) et C (la catégorisation). Aucune migration — tous les champs sont additifs.
Feature A — Le forfait qui déclenche la sélection marque/modèle
plombier & électricien · le seul vrai chantier du lot
Ce que Saber a demandé (call 16/07)
« Quand on crée un forfait ici, c'est là où on va indiquer, dans le forfait, est-ce que ça va déclencher l'interface qui arrive en bas ou pas. »— Saber, 27:38 (validé par Joseph : « Ok, pas de souci »)
« Les produits évoluent, il faut absolument qu'on puisse [les gérer côté admin]. […] Quand on le fait nous-mêmes, c'est restreint : le professionnel sait déjà les produits avec lesquels on va travailler, il pourra anticiper d'avoir ses produits dans son véhicule. »— Saber, 18:26 & 22:52 · liste fermée voulue, pas de champ libre client
« Mon vrai souci c'est les deux services très importants : plombier, électricien. Tout le reste c'est gérable normal. »— Saber, 26:13 · le mécanisme doit rester optionnel, forfait par forfait
Décision de conception
Un produit « Classique Bosch — 10 € » est déjà exprimable : c'est un
item d'un groupe à pricing fixed/unit
(nom i18n + prix + actif — Saber a validé le texte+prix en séance, 24:07).
La seule brique manquante est le lien :
quel forfait déclenche quel groupe de produits.
// src/models/subdocuments/item.js — champ additif product_group_id: { type: Schema.Types.ObjectId, default: null, } // null = forfait normal (aucune interface déclenchée) // sinon = _id d'un AUTRE groupe du MÊME service, // dont les items sont les marques/modèles proposés
Alternative rejetée — et pourquoi
Collection « products » dédiée (marque, modèle, photo…) —
rejetée. Elle dupliquerait ce que groupes/items font déjà (i18n, prix, actif,
contribution au priceEngine), imposerait un 2ᵉ CRUD admin, une
migration, et un nouveau contrat mobile. Le lien réutilise 100 % de
l'existant : l'admin gère les produits dans l'écran Tarifs qu'il connaît
déjà, et le prix des produits entre dans le calcul sans une ligne de plus
(les sélections d'items sont déjà sommées par priceEngine.calculate).
Règles de validation (Joi + contrôleur)
product_group_iddoit référencer un groupe du même service- jamais le groupe qui contient le forfait lui-même (auto-référence)
- groupe cible en pricing
fixedouunit(pashourly/info) - suppression d'un groupe référencé → 400 avec la liste des forfaits liés
Contrat mobile (pour Achille) — payload service, champ additif
GET /api/v2/services/:id // shape actuelle inchangée, + 1 champ par item { groups: [ { _id: "g1", name: {...}, pricing: "fixed", type: "single", items: [ { _id: "i1", name: { fr: "Forfait remplacement WC" }, price: 120, product_group_id: "g7" } // ← déclenche l'interface ]}, { _id: "g7", name: { fr: "Marque du produit" }, pricing: "fixed", type: "single", items: [ { _id: "i9", name: { fr: "Classique Bosch" }, price: 10 }, { _id: "i10", name: { fr: "Classique Giesman" }, price: 20 } ]} ] } // Mobile : à la sélection d'un item dont product_group_id ≠ null, // afficher les items du groupe cible (choix unique si type "single"). // Le prix choisi s'ajoute au total — le backend le somme déjà via selections[].
Fichiers touchés
- Backend ·
subdocuments/item.js,validations/services.js,serviceController(règles ci-dessus) - Admin · formulaire forfait de
dashboard/tarifs: select « Déclenche une sélection produit » listant les groupes éligibles du service - Tests · unit validation (4 règles) + intégration
services(round-trip du champ) + un caspriceEngineforfait+produit sommés
Critères d'acceptation
- L'admin crée « Forfait remplacement WC » lié au groupe « Marques », sans toucher à la base
- Le payload service expose
product_group_idet le mobile peut afficher la liste fermée - Un forfait sans lien se comporte exactement comme aujourd'hui (aucune régression sur les 14 autres services)
- Supprimer le groupe « Marques » est refusé tant qu'un forfait le référence
Feature B — L'icône du forfait implémentée le 17/07
livrée de bout en bout côté admin · il reste l'affichage mobile
« Dès qu'on crée le forfait, on choisit l'icône qui correspond. Les icônes qui correspondent au forfait, c'est des icônes qui sont modifiables côté admin. »— Saber, 1:05:44
Ce qui a été livré
- Backend ·
validations/groups.js:iconsaccepté sur addItem/updateItem (tableau d'ids Icon) ·groupController: écriture à la création et à l'édition · suite d'intégrationitemsverte (9/9) - Admin · picker d'icône dans le dialogue d'option (
item-dialog.tsx) : grille de la bibliothèque, sélection visuelle, bouton « Aucune » · nouveau proxyGET /api/iconesqui construit les URLs publiques · hookuseIcones()(cache 10 min) - Contrat · le BFF aplatit
icons[0]enicon_idpour l'admin ; le payload service mobile gardeicons: [iconId]inchangé - Fix découvert à la capture ·
files/publicrefusait les clésicons/(403) — préfixe ajouté, sans quoi ni l'admin ni le mobile ne pouvaient afficher les icônes
item.icons[0] (mode d'emploi ci-dessous).
Pour Achille — afficher l'icône côté mobile
1. GET /api/v2/icons (authentifié, une fois, à cacher) → { data: [{ _id, icon: "icons/cle.png", mode }] } 2. URL d'affichage (fichier public, pas d'auth) : {BASE}/api/v2/files/public?key={encodeURIComponent(icon)} 3. item.icons[0] → lookup dans le catalogue → <Image url /> icons vide → pas d'icône, fallback libre
Reste à faire (hors mobile)
- Écran d'upload de la bibliothèque — le backend est prêt (
POST /icons, CRUD complet) mais aucun écran admin ne permet d'ajouter une icône. Petit écran dans Paramètres (~½ j) — sans lui, la bibliothèque se remplit via l'API.
Critères d'acceptation (admin — vérifiés)
- Créer/éditer une option permet de choisir une icône ou « Aucune »
- Un item sans icône reste valide, zéro impact sur l'existant (tests verts)
Feature C — Catégorisation des groupes & options
les « catégories principales » de Saber, généralisées à tous les services
Ce que Saber a décrit (call 16/07)
« Au début on a la catégorie — WC, Lavabo, etc. Maintenant tu as les forfaits… et après tu as remplacement, c'est à la carte. Tu mets un cadre où il n'y a que les forfaits, et en bas un autre cadre à la carte. »— Saber, 1:01:51 → 1:07:00 · la catégorie principale filtre ce qui s'affiche dessous
« Tu peux mettre carrément un autre carousel, un truc clair, comme quoi il est rentré dans sa catégorie. »— Saber, 1:05:23 · la catégorie est un niveau de navigation, pas un simple libellé
Décision de conception
Un niveau catégorie au-dessus des groupes, propre à chaque service, et un lien optionnel sur le groupe — même philosophie additive que le lien produit de la feature A.
// service.js — sous-document additif categories: [{ name: i18nSchema, // "WC", "Lavabo"… ordre: Number, // ordre du carousel }] // group.js — lien optionnel category_id: { type: Schema.Types.ObjectId, default: null, // null = hors catégorie, } // affiché en permanence
Alternative rejetée — et pourquoi
Collection Category globale partagée entre services — rejetée. Les catégories de Saber sont intrinsèques à un service (« WC » n'a de sens que pour plombier) ; une collection partagée imposerait des jointures, un CRUD global et des suppressions en cascade inter-services. Le sous-document par service suit le pattern groups/items existant que l'admin sait déjà éditer.
Règles
category_iddoit référencer une catégorie du même service- suppression d'une catégorie → ses groupes repassent à
null(pas de cascade destructive) - service sans catégories → affichage à plat, strictement comme aujourd'hui (14 services intouchés)
Contrat mobile (pour Achille)
GET /api/v2/services/:id
{
categories: [ // trié par ordre — le carousel
{ _id: "c1", name: { fr: "WC" }, ordre: 1 },
{ _id: "c2", name: { fr: "Lavabo" }, ordre: 2 }
],
groups: [
{ _id: "g1", category_id: "c1", pricing: "fixed", type: "single", // forfaits WC
items: [...] },
{ _id: "g2", category_id: "c1", pricing: "fixed", type: "multi", // à la carte WC
items: [...] },
{ _id: "g9", category_id: null, ... } // affiché quelle que soit la catégorie
]
}
// Rendu : carousel des categories (si non vide) → groupes de la
// catégorie sélectionnée + groupes à null. categories vide → à plat.
// Se compose avec A : un forfait WC peut pointer son product_group_id.
Fichiers touchés
- Backend ·
models/service.js(+ sous-schéma),subdocuments/group.js, CRUD catégories dansserviceControllerou routes groups, validations Joi - Admin ·
parametres/services/[id]: gestion des catégories (liste ordonnable) + select catégorie sur le formulaire de groupe - Tests · règles de lien (même service, remise à null) + round-trip payload
Critères d'acceptation
- L'admin crée « WC », « Lavabo », les ordonne, et rattache ses groupes
- Le payload service expose
categories[]+category_idet le mobile peut rendre le carousel - Supprimer une catégorie ne supprime aucun groupe
- Les services sans catégories sont rendus exactement comme avant
Ordre de réalisation — trié par qui il débloque
| Tâche | Débloque | Effort | Risque | |
|---|---|---|---|---|
| 1 | A-backend — champ + validation + contrat produit | Achille (interface marque/modèle) dès le contrat figé | ~½ j | faible — champ additif |
| 2 | C-backend — categories[] + category_id + contrat | Achille (navigation par catégories) | ~½ j | faible — même pattern que A |
| 3 | A-admin + C-admin — select produit sur forfait, gestion des catégories, select catégorie sur groupe (même écran Paramètres → Service, un seul passage) | Saber (saisie réelle plombier/électricien) | ~1-1½ j | faible — écran existant |
| 4 | B-upload — petit écran bibliothèque d'icônes (backend prêt) | Saber (autonomie sur les icônes) | ~½ j | minime |
| 5 | Tests + captures + recette — même méthode que le lot virements | — | ~½ j | — |
Total : ≈ 3 jours côté Joseph (la feature B icônes est déjà livrée, la catégorisation s'y substitue). Les étapes 1-2 (backend) débloquent Achille dès le premier jour : le mobile et l'admin avancent ensuite en parallèle sur contrats figés, comme sur le lot virements.
Garde-fous & questions restantes
Ce que ce lot ne casse pas
- Zéro migration — trois champs additifs, valeurs par défaut inertes
- Zéro breaking change mobile — payloads existants inchangés, champs ajoutés uniquement
- 14 services non concernés — sans lien produit, comportement strictement identique
À trancher avec Saber (2 minutes en début de lot)
- Le groupe produits est-il à choix unique (une marque par forfait) ? — le call le suggère, le champ
type: "single"existe déjà pour ça - Le prix produit s'ajoute au forfait (Bosch +10 €) ou le remplace ? — le call montre des prix par produit, on part sur l'addition (comportement actuel du moteur)
- La catégorisation vaut-elle pour tous les services ou seulement plombier/électricien ? — le modèle la rend disponible partout sans l'imposer nulle part, mais autant confirmer l'usage voulu
Plan établi le 17/07/2026 sur l'état réel du code (branches integration/all-fixes backend ·
feat/admin-transactions frontend) et la transcription du call du 16/07.
Fait suite au lot « virements pros » (rapport séparé).