Gestion des restes : portions demandées vs préparées + panier de restes en drag & drop sur le planning #34
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é
Aujourd'hui,
PlanningItem.portionsest un unique nombre saisi librement àl'ajout d'une recette au planning : « combien de portions préparer pour ce
créneau », sans lien imposé avec
Recipe.portions(le rendement écrit de larecette). La liste de courses et l'optimiseur de session de cuisine mettent
juste les quantités d'ingrédients à l'échelle (
portions / recipe.portions),y compris fractionnaire (ex. 3/4 d'une recette 4 portions).
Cette fonctionnalité introduit une deuxième valeur — combien de portions
sont réellement nécessaires pour ce créneau — et fait ressortir l'écart
avec ce qui est effectivement préparé (toujours un multiple entier du
rendement de la recette, on ne cuisine jamais une fraction d'un plat) comme
des restes, rassemblés semaine par semaine dans un panier accessible
depuis le planning, attribuables ensuite à un autre créneau par glisser-déposer.
Comportement actuel (rappel)
Recipe.portions: rendement de la recette telle qu'écrite (ex. 4 pour deslasagnes).
PlanningItem.portions: saisi indépendamment à l'ajout (pré-rempli avecRecipe.portions, mais librement modifiable) — voirplanning.service.ts et
RecipePickerDialog.tsx:376-409.
portions d'une recette pour 4, ni 10. La liste de courses et l'optimiseur de
cuisine scalent alors les quantités d'ingrédients par ce ratio, y compris
fractionnaire — voir
aggregateShoppingList(shopping-list.service.ts:62-90)
et
toOptimizerRecipe(cooking-session.service.ts:75-118).
Comportement souhaité
À l'ajout d'une recette sur un créneau, on ne saisit plus qu'un seul
nombre : les portions réellement demandées (ex. « on est 3 » / « il en faut
10 »). L'application calcule elle-même combien de portions seront
effectivement préparées, en arrondissant au multiple supérieur du
rendement de la recette — on ne cuisine jamais une fraction de plat, mais on
peut en cuisiner plusieurs exemplaires :
Vérification sur les deux exemples de la demande initiale (recette 4
portions) :
Ce reste alimente un panier — une icône en haut de
PlanningPage, quiouvre un popover listant, pour la semaine affichée, chaque créneau ayant
encore des portions de reste disponibles (recette, jour/repas d'origine,
quantité restante). Depuis ce panier :
l'y attribue : au dépôt, une petite étape confirme la quantité à assigner
(par défaut la totalité disponible, ajustable jusqu'à ce maximum — même
esprit que l'étape « combien de portions ? » déjà existante), puis crée une
vraie entrée dans la grille pour ce créneau, sur la même recette, marquée
visuellement comme « restes ». Rien à cuisiner ni à acheter pour ce
créneau-là : il est exclu de la liste de courses et de la session de
cuisine (voir plus bas).
le retire du panier sans l'attribuer à un créneau (ex. denrée jetée,
finalement pas mangée).
Décisions de conception (validées avec l'utilisateur)
ceil(demandées / recette.portions) × recette.portions), un seul champ saisiPlanningItemdans la grille, même recette, marquée « restes », exclue liste de courses + session de cuisineplanning.items) plutôt qu'un modèle de restes parallèle@dnd-kit/core(nouvelle dépendance)StepListEditor, utilise des boutons haut/bas pour éviter d'en ajouter une) ; support tactile/accessibilité clavier inclus, et interactions pilotables en Cypress (pointer events simulables) — le HTML5 natif est notoirement difficile à tester en e2eModèle de données
Colonnes ajoutées sur
PlanningItem(apps/api/prisma/schema.prisma) —purement additives, aucune colonne existante renommée :
Migration —
portions_requesteddoit êtreNOT NULLmais la table a deslignes existantes ; suivre la même discipline que la migration
ingredient_unit_catalogdocumentée dans schema.prisma (ajouter nullable,backfill, puis verrouiller) :
PlanningItemView(packages/shared/src/types/planning.ts) — le JSDoc deportionsdoit aussi être corrigé (il n'est plus "une saisie manuelle libre"pour un item frais, mais toujours un multiple de
recipe.portions) :Pas besoin d'un
includePrisma supplémentaire côté lecture : ces 4 champssuffisent, calculés à la volée côté front à partir du tableau plat
planning.itemsdéjà chargé (voir logique partagée ci-dessous) — aucun allerDB de plus pour
GET /planning.Logique pure partagée (
packages/shared— colocaliser avectypes/planning.ts, à côté deWEEK_DAYS/MEALSqui y vivent déjà, ou dansun nouveau fichier voisin si la revue préfère séparer types et logique) :
Backend
Nouvelles routes sur
apps/api/src/modules/planning/planning.routes.ts:POST /planning/items/leftoverassignLeftoverPlanningItem(date, weekDay, meal)depuissourceItemId, pourportions(≤ disponible, revalidé serveur viacomputeAvailableLeftover— jamais confiance dans une valeur envoyée par le client). 404PLANNING_ITEM_NOT_FOUNDsi la source n'existe pas / n'appartient pas au foyer, 409LEFTOVER_QUANTITY_EXCEEDED(nouveau code, plage 402x) siportionsdépasse le disponible.recipeIdn'est jamais envoyé par le client : copié depuis la source.PATCH /planning/items/:id/discard-leftoverdiscardLeftover:id(discardedPortions += disponible) ; idempotent si déjà à 0. 404 même garde queremovePlanningItem.Changements sur l'existant :
addPlanningItem— appliquecomputeBatchedPortions, stockeportions(calculé) et
portionsRequested(saisi) au lieu duportionsbrutd'aujourd'hui.
getPlanningForDate— ajouteportionsRequested/discardedPortions/sourceItemà la forme dePlanningItemView(pas de nouvelinclude, voirplus haut).
removePlanningItem— aucun changement de code :onDelete: CascadesursourcePlanningItemIdsupprime déjà les items "restes" dépendants.shopping-list.service.ts(aggregateShoppingList/getShoppingListForDate) — doit exclure les items "restes"(
sourcePlanningItemId !== null) avant agrégation : rien de nouveau àacheter pour un créneau qui ne fait que resservir un plat déjà cuisiné.
Sans ce filtre, les ingrédients seraient comptés en double. Test de
non-régression obligatoire (facile à casser silencieusement).
cooking-session.service.ts(getCookingPlanForDate, avantplanning.items.map(toOptimizerRecipe)) — même exclusion : un créneau"restes" n'a aucune étape à cuisiner, il ne doit pas apparaître dans le
plan optimisé de la session de cuisine.
packages/shared/src/errors/error-codes.ts— nouveau membreLEFTOVER_QUANTITY_EXCEEDED = 4024(plage 4020-4029, conflit d'état — prochainnuméro libre après
RETRAIN_ALREADY_RUNNING = 4023), + traduction dansapps/web/src/locales/fr/translation.json(errors.*).analytics.service.ts— deux nouveaux événements, même esprit queplanning.item_added:planning.leftover_assigned(
{ recipeId, portions, sourceItemId }) etplanning.leftover_discarded(
{ recipeId, portions }).Frontend
apps/web/src/pages/planning/PlanningPage.tsx— ajoute l'icône panierdans
.planning-page__header(à côté deWeekNavigator/bouton"cuisiner") ; englobe la grille + le panier dans un
<DndContext>(
@dnd-kit/core) pour que le glisser-source (carte du panier) et lesdépôts (cellules de la grille) communiquent.
apps/web/src/features/planning/LeftoversBasket.tsx(nouveau) — icôneWeekNavigator'sCalendarPopover(WeekNavigator.tsx:93-174) :
état
isOpenlocal, montage conditionnel, fermeture au clic extérieur viauseRef+listenermousedown. Liste chaque item deplanning.itemsdontsourceItem === nulletcomputeAvailableLeftover(...) > 0: recette,jour/repas d'origine, quantité disponible, carte draggable
(
useDraggable), bouton "jeter" (→PATCH .../discard-leftover).PlanningGrid/MealCell(PlanningPage.tsx) — chaque cellule devientune zone de dépôt (
useDroppable), retour visuel au survol pendant undrag ; un item "restes" (
sourceItem !== null) affiche un badge distinct(ex. ♻️) sur son
recipe-chip. Au dépôt, petite étape de confirmation dequantité (par défaut le disponible, plafonné à celui-ci) avant l'appel à
POST /planning/items/leftover.RecipePickerDialog.tsx(étape "combien de portions ?",lignes 376-409)
et
RecipeImportForm.tsx— champ renommé "portions demandées" ; ajouteun texte dérivé en direct via
computeBatchedPortions(ex. « → 12 portionspréparées, 2 en restes ») affiché seulement si
leftover > 0; envoientportionsRequested(plusportions) au lieu deportionsseul.apps/web/package.json— ajoute@dnd-kit/core(+@dnd-kit/utilitiessi besoin des helpers de transform CSS),
pnpm installà la racine.planning.leftovers.*dansapps/web/src/locales/fr/translation.json: icône/tooltip du panier, titrepopover, état vide, libellé par ligne, bouton "jeter" (+ confirmation),
invite de quantité au dépôt, badge "restes" sur un chip, libellés
d'accessibilité des poignées de drag (support clavier
@dnd-kit).KeyboardSensorde@dnd-kiten plus duPointerSensor(Espace pour saisir, flèches pour déplacer, Espace pourdéposer) : ce n'est quasiment pas de travail en plus avec cette lib et
évite une fonctionnalité clavier-inaccessible.
Découpage proposé (chantier en plusieurs PRs, comme les précédents)
Même logique que les chantiers "cooking-session" (#22-28) ou "tech-step
metadata editor" (#29-32) : petites PRs scopées plutôt qu'une seule grosse.
portionsRequested/calcul par lot(
packages/shared,addPlanningItem,getPlanningForDate) + mise à jourdes deux points d'appel front (
RecipePickerDialog,RecipeImportForm) +specs Cypress existantes (fixtures
POST /planning/itemsà mettre àjour). Aucune UI de panier/restes encore visible.
POST .../leftover+PATCH .../discard-leftover, nouveauErrorCode, exclusionliste de courses + session de cuisine, événements analytics.
LeftoversBasket.tsx(icône, popover, liste,bouton "jeter"), sans glisser-déposer — juste consultation + jet.
@dnd-kit/core,DndContext, cellulescomme zones de dépôt, badge "restes" sur la grille, e2e Gherkin complet.
Tests (obligatoires par PR, cf.
specs/dev-conventions.md)computeBatchedPortions/computeAvailableLeftover(cas limites : demandé multiple exact → reste 0 ; demandé < portions
recette → 1 lot ; demandé > plusieurs lots) ;
addPlanningItemstocke bienles deux valeurs ;
assignLeftoverPlanningItem(succès, 409 surdépassement, 404 source étrangère/inexistante,
recipeIdjamais pris duclient) ;
discardLeftover(réduit à 0, idempotent) ; suppression encascade d'un item source ; régression
aggregateShoppingListetgetCookingPlanForDateignorant bien les items "restes".LeftoversBasket(mockGET /planningviacy.interceptavec un item à reste disponible ;ouverture/fermeture popover, clic extérieur, bouton "jeter") ; scénario
Gherkin e2e (nouveau
.feature, ou extension deplanning.feature) pourle parcours complet ajout → reste visible au panier → glisser sur un autre
créneau → confirmation quantité → chip "restes" visible → panier décrémenté
→ jeter le reste restant → panier vide.
@dnd-kitexpose des interactionspilotables par événements pointer simulables en Cypress (contrairement au
drag-and-drop HTML5 natif, notoirement pénible à tester en e2e) — un
argument de plus en faveur de ce choix.
Documentation à mettre à jour
specs/batch-cooking-modele.md— entitéplanning_item(3 nouvellescolonnes + auto-relation).
specs/backend-architecture.md— section "planning — semaine, item,portions, import à la volée" (règle de lot, nouvelles routes, exclusions
liste de courses/session de cuisine).
specs/frontend-architecture.md— arborescencefeatures/planning/(ajoutLeftoversBasket.tsx), description de l'étape "portions" enrichie.Hors scope pour cette itération
reste non attribué/non jeté ne "voyage" pas vers la semaine suivante dans
cette V1 — à réévaluer si le besoin se confirme à l'usage).
clic ;
discardedPortionsest modélisé comme une quantité, pas unbooléen, donc un jet partiel reste possible à ajouter plus tard sans
nouvelle migration).
sert que ce qui a été effectivement cuisiné).
Points restant à trancher en revue de code (non bloquants pour démarrer)
POST /planning/items/leftoverproposépar symétrie avec
POST /planning/items— pas de collision possible avecDELETE /planning/items/:id, méthodes différentes, mais à valider que çalit bien).
computeBatchedPortions/computeAvailableLeftoverdans
packages/shared(proposé :types/planning.ts, aux côtés deWEEK_DAYS/MEALS).style de
.recipe-picker-confirm, ou un composant dédié plus léger).🤖 Filed by Claude Code
Gestion des restes : portions demandées vs préparées + panier de restes en drag & drop sur le planningto Gestion des restes : portions demandées vs préparées + panier de restes en drag & drop sur le planning