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.

1

État des lieux — vérifié dans le code, pas supposé

FonctionnalitéModèle MongoAPI backendAdmin UIMobile (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à fait
item.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.

2

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_id doit 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 fixed ou unit (pas hourly/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 cas priceEngine forfait+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_id et 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
3

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 : icons accepté sur addItem/updateItem (tableau d'ids Icon) · groupController : écriture à la création et à l'édition · suite d'intégration items verte (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 proxy GET /api/icones qui construit les URLs publiques · hook useIcones() (cache 10 min)
  • Contrat · le BFF aplatit icons[0] en icon_id pour l'admin ; le payload service mobile garde icons: [iconId] inchangé
  • Fix découvert à la capture · files/public refusait les clés icons/ (403) — préfixe ajouté, sans quoi ni l'admin ni le mobile ne pouvaient afficher les icônes
Le picker d'icônes dans le dialogue d'option du panel admin
L'implémentation livrée, en vrai — Paramètres → Service → « Ajouter une option » : la grille d'icônes de la bibliothèque, la sélection surlignée, le bouton « Aucune ». C'est ce que Saber utilisera pour rattacher une icône à chaque forfait — et ce que le mobile recevra dans 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)
4

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_id doit 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 dans serviceController ou 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_id et le mobile peut rendre le carousel
  • Supprimer une catégorie ne supprime aucun groupe
  • Les services sans catégories sont rendus exactement comme avant
5

Ordre de réalisation — trié par qui il débloque

TâcheDébloqueEffortRisque
1A-backend — champ + validation + contrat produitAchille (interface marque/modèle) dès le contrat figé~½ jfaible — champ additif
2C-backend — categories[] + category_id + contratAchille (navigation par catégories)~½ jfaible — même pattern que A
3A-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½ jfaible — écran existant
4B-upload — petit écran bibliothèque d'icônes (backend prêt)Saber (autonomie sur les icônes)~½ jminime
5Tests + 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.

6

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é).