Quand le catalogue seede ne couvre pas un ingredient, l'utilisateur pouvait etre bloque (creation manuelle) ou perdre silencieusement la ligne (import). Une ligne de recette accepte desormais `placeholderName` (texte libre) au lieu de `ingredientId` : l'API cree une ligne `Ingredient` `isPlaceholder` (cle `placeholder:<uuid>`, `displayName`, `createdById`) dans la transaction de la recette, et emet `ingredient.placeholder_created`. Ces lignes sont exclues de `GET /reference/ingredients` et de `ingredient-matcher`. Front : bouton "Ajouter << ... >>" dans l'etat vide de `IngredientPicker` (formulaire + import), badge "a completer" sur la ligne, helper `ingredientLabel` applique partout ou un libelle d'ingredient est rendu. Admin : `/admin/catalog/*` (+ page `apps/admin-web`) liste les placeholders regroupes par nom normalise, "marquer traite" (`reviewedAt`) et purge des orphelins. La promotion en vraie entree catalogue reste manuelle. Migration `ingredient_placeholder` ecrite a la main (Postgres indisponible). Suites Mocha DB-backed ecrites, non executees en session ; test pur `normalizePlaceholderName` + Cypress admin-web/web verts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
44 lines
1.4 KiB
TypeScript
44 lines
1.4 KiB
TypeScript
import { wrapAsyncHandler } from "@batch-cooking/express-tools";
|
|
import { listPlaceholdersQuerySchema, markPlaceholdersReviewedSchema } from "@batch-cooking/shared";
|
|
import { Router } from "express";
|
|
import { requireAdmin } from "../../middlewares/require-admin.js";
|
|
import {
|
|
listPlaceholderGroups,
|
|
markPlaceholdersReviewed,
|
|
pruneOrphanPlaceholders,
|
|
} from "./admin-catalog.service.js";
|
|
|
|
/**
|
|
* Router mounted at `/admin/catalog` (via `admin.routes.ts`) — every route
|
|
* behind {@link requireAdmin}. Surfaces the off-catalog ingredient
|
|
* "placeholders" users typed when the seeded catalog fell short, so a
|
|
* maintainer can see what's missing and mark gaps as handled.
|
|
*/
|
|
export const adminCatalogRouter = Router();
|
|
|
|
adminCatalogRouter.get(
|
|
"/placeholders",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
res.status(200).json(await listPlaceholderGroups(listPlaceholdersQuerySchema.parse(req.query)));
|
|
}),
|
|
);
|
|
|
|
adminCatalogRouter.patch(
|
|
"/placeholders/mark-reviewed",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (req, res) => {
|
|
res
|
|
.status(200)
|
|
.json(await markPlaceholdersReviewed(markPlaceholdersReviewedSchema.parse(req.body)));
|
|
}),
|
|
);
|
|
|
|
/** Deletes placeholder rows no recipe references any more — see {@link pruneOrphanPlaceholders}. */
|
|
adminCatalogRouter.post(
|
|
"/placeholders/prune-orphans",
|
|
requireAdmin,
|
|
wrapAsyncHandler(async (_req, res) => {
|
|
res.status(200).json(await pruneOrphanPlaceholders());
|
|
}),
|
|
);
|