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>
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
import { parseDateOnly } from "@batch-cooking/date-tools";
|
|
import { HttpError } from "@batch-cooking/error-tools";
|
|
import { wrapAsyncHandler } from "@batch-cooking/express-tools";
|
|
import { ErrorCode, addPlanningItemSchema, getPlanningByDateSchema } from "@batch-cooking/shared";
|
|
import { Router } from "express";
|
|
import { type AuthLocals, requireAuth } from "../../middlewares/require-auth.js";
|
|
import { addPlanningItem, getPlanningForDate, removePlanningItem } from "./planning.service.js";
|
|
|
|
/** Router mounted at `/planning` in app.ts. */
|
|
export const planningRouter = Router();
|
|
|
|
/**
|
|
* Returns the authenticated user's household's planning covering `?date=`
|
|
* (`YYYY-MM-DD`), or `null` if none exists yet — a valid, common response,
|
|
* not an error (see {@link getPlanningForDate}). Used both for "today"
|
|
* (the planning page's initial load) and for any other week the planning
|
|
* page's week navigator/calendar picks.
|
|
*/
|
|
planningRouter.get(
|
|
"/",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = getPlanningByDateSchema.parse(req.query);
|
|
const date = parseDateOnly(input.date);
|
|
if (date === null) {
|
|
throw new HttpError(
|
|
400,
|
|
ErrorCode.VALIDATION_ERROR,
|
|
`Not a real calendar date: ${input.date}`,
|
|
);
|
|
}
|
|
|
|
const planning = await getPlanningForDate(res.locals.userProfile.houseId, date);
|
|
res.status(200).json(planning);
|
|
}),
|
|
);
|
|
|
|
/** Parses and validates the `:id` route param shared by every `/items/:id` route below. */
|
|
function parsePlanningItemId(rawId: string | undefined): number {
|
|
const id = Number(rawId);
|
|
if (!Number.isInteger(id)) {
|
|
throw new HttpError(400, ErrorCode.VALIDATION_ERROR, "id must be an integer");
|
|
}
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Adds a recipe to one (day, meal) slot of the authenticated user's
|
|
* household's planning, creating that week's `Planning` row on the fly if
|
|
* needed (see {@link addPlanningItem}).
|
|
*/
|
|
planningRouter.post(
|
|
"/items",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const input = addPlanningItemSchema.parse(req.body);
|
|
const date = parseDateOnly(input.date);
|
|
if (date === null) {
|
|
throw new HttpError(
|
|
400,
|
|
ErrorCode.VALIDATION_ERROR,
|
|
`Not a real calendar date: ${input.date}`,
|
|
);
|
|
}
|
|
|
|
const { id: viewerId, houseId } = res.locals.userProfile;
|
|
const item = await addPlanningItem(houseId, viewerId, houseId, date, input);
|
|
res.status(201).json(item);
|
|
}),
|
|
);
|
|
|
|
/** Removes one recipe from a planning slot. */
|
|
planningRouter.delete(
|
|
"/items/:id",
|
|
requireAuth,
|
|
wrapAsyncHandler<unknown, AuthLocals>(async (req, res) => {
|
|
const id = parsePlanningItemId(req.params.id);
|
|
await removePlanningItem(id, res.locals.userProfile.houseId);
|
|
res.status(204).end();
|
|
}),
|
|
);
|