batchCooking/apps/web/src/features/recipes/ingredients/ingredient-label.ts
Nicolas 16f593d5d0
Some checks failed
CI / lint (push) Successful in 1m46s
CI / build (push) Successful in 1m55s
CI / e2e (push) Successful in 8m37s
CI / intent-service-test (push) Successful in 17m3s
CI / test (push) Failing after 3h14m19s
feat(recipes): permet d'ajouter des ingredients hors-catalogue
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>
2026-08-28 23:36:42 +02:00

26 lines
1.2 KiB
TypeScript

import type { IngredientView } from "@batch-cooking/shared";
/**
* The display name for an ingredient line.
*
* A real catalog ingredient has no name in the database — its French label
* lives in i18n under `catalog.ingredients.<key>` — so it's resolved
* through `t`. A **placeholder** (`IngredientView.isPlaceholder`, the
* free text a user typed when the catalog fell short — see
* `Ingredient.isPlaceholder` in schema.prisma) has its name stored on the
* row as `displayName` and no i18n key at all, so it's shown verbatim.
*
* Every place that used to inline `t(\`catalog.ingredients.${ingredient.key}\`)`
* goes through this instead, so a placeholder never renders as the raw,
* missing translation key `catalog.ingredients.placeholder:<uuid>`.
*
* `t` is passed in (rather than calling `useTranslation` here) so this
* stays a plain function usable from non-component code and unit-testable
* without mounting i18next — same split as `shopping-list.ts`.
*/
export function ingredientLabel(
ingredient: Pick<IngredientView, "key" | "displayName">,
t: (key: string) => string,
): string {
return ingredient.displayName ?? t(`catalog.ingredients.${ingredient.key}`);
}