PR 2/4 du chantier « gestion des restes » (issue #34).
Deux nouvelles routes sur `/planning` :
- `POST /planning/items/leftover` → `assignLeftoverPlanningItem`, 201.
Crée un item « restes » (`sourcePlanningItemId` non nul) sur un créneau,
même recette que la source (`recipeId` copié, jamais envoyé par le
client). `portions` revérifié serveur contre le pool réellement
disponible via `computeAvailableLeftover` → 409
`LEFTOVER_QUANTITY_EXCEEDED` (nouveau code, 4024) si dépassé ; 404
`PLANNING_ITEM_NOT_FOUND` si source inconnue / d'un autre foyer / elle-
même un item « restes » (pas de restes-sur-restes).
- `PATCH /planning/items/:id/discard-leftover` → `discardLeftover`, 200.
Jette tout le pool restant (`discardedPortions += disponible`) ;
idempotent (rien à jeter → renvoyé inchangé, aucun évènement).
Exclusion : `shopping-list.service.ts` et `cooking-session.service.ts`
filtrent `items: { where: { sourcePlanningItemId: null } }` dans leur
`include` — un créneau qui ne fait que resservir un plat déjà cuisiné n'a
rien à acheter ni à cuisiner. `getPlanningForDate` ne filtre pas (la
grille affiche ces items).
Analytics : `planning.leftover_assigned` / `planning.leftover_discarded`.
Le mapping `PlanningItem` → `PlanningItemView` est factorisé
(`toPlanningItemView`) — 4 chemins le partagent désormais.
Tests (Mocha, DB) : `assignLeftoverPlanningItem` (succès + `recipeId`
copié, 409 sur dépassement, 404 source étrangère/inconnue/« restes »,
cascade à la suppression de la source) ; `discardLeftover` (jette le
disponible, idempotent, ne jette pas ce qui est déjà servi, 404) ;
non-régression liste de courses et session de cuisine ignorant les items
« restes ».
Réf #34
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Première PR du chantier « gestion des restes » (issue #34) — les
fondations, sans encore d'UI de panier.
À l'ajout d'une recette au planning on ne saisit plus qu'un seul nombre,
`portionsRequested` (« combien ce créneau doit nourrir »). Le serveur en
dérive `portions` (ce qui est réellement préparé) via
`computeBatchedPortions` : arrondi au multiple supérieur du rendement de
la recette (on cuisine des lots entiers, jamais une fraction de plat).
L'écart est le futur pool de restes de l'item.
Modèle (`PlanningItem`, purement additif) :
- `portionsRequested` NOT NULL — migration nullable → backfill (`= portions`
pour les lignes existantes, sans restes à l'époque) → verrou, même
discipline que `ingredient_unit_catalog`.
- `sourcePlanningItemId` (auto-FK nullable, `onDelete: Cascade`) et
`discardedPortions` (défaut 0) — posés maintenant, utilisés plus tard.
Partagé (`@batch-cooking/shared/logic/planning-portions.ts`) :
- `computeBatchedPortions(recipePortions, requested)` → `{ portions, leftover }`
- `computeAvailableLeftover(item)` → pool encore disponible (posé pour les
PRs suivantes : validation d'écriture côté API, affichage du panier
côté web — une seule formule, deux appelants)
API : `addPlanningItemSchema` prend `portionsRequested` ; `addPlanningItem`
calcule et stocke les deux valeurs + l'évènement analytics ; `PlanningItemView`
et `getPlanningForDate` exposent `portionsRequested`/`discardedPortions`/
`sourceItem`.
Front : `RecipePickerDialog` — champ « portions demandées » + indice en
direct « → N préparées, M en restes » quand l'arrondi crée un reste ;
envoie `portionsRequested`. `RecipeImportForm` — envoie `portionsRequested`
(un import atterrit à son propre rendement, donc reste nul).
Tests : `planning-portions.test.ts` (les deux exemples de #34 + bornes) ;
nouveau bloc `POST /planning/items` dans `planning.test.ts` (arrondi au
lot, plusieurs lots, borne exacte, 400 sur non-positif) ; fixtures
`planningItem.create` des suites existantes mises à jour ; scénario
Cypress planning ajusté (`portionsRequested` dans le corps).
Docs : `batch-cooking-modele.md`, `backend-architecture.md`,
`frontend-architecture.md`.
Réf #34
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute le chaînon manquant entre le catalogue de recettes et le planning
hebdomadaire :
- Backend : `PlanningItem.portions` (nouvelle colonne + migration),
`POST /planning/items` / `DELETE /planning/items/:id` (créent la
semaine de planning à la volée si besoin), `GET /recipes` gagne les
filtres `ingredientIds`/`dietIds` (ET) en plus de `suitableForHousehold`
(déjà préparé).
- Frontend : nouveau `Dialog` générique (premier modal de l'app),
`RecipePickerDialog` qui réutilise le même affichage que le catalogue
(`RecipeTabs`/`RecipeTable`) avec recherche par nom, filtre ingrédients,
filtre régime alimentaire, toggle "convient à tout le foyer", puis une
étape de saisie du nombre de portions.
- `PlanningPage` : le bouton "+" de chaque case ouvre le dialog, le
bouton "✕" retire la recette (optimiste, avec rollback si l'appel
échoue), les portions s'affichent sur chaque chip.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- packages/date-tools: parseDateOnly/formatDateOnly/toDateOnly,
getWeekStart/addWeeks/buildCalendarMonth, sur Luxon DateTime (UTC)
- packages/shared: WEEK_DAYS/WeekDay, MEALS/Meal (contrat de valeurs
documenté pour PlanningItemView.weekDay/.meal, pas encore enforcé
en base), getPlanningByDateSchema (validation de forme de ?date=)