feat(recipe): Edit-Modus für Zutaten, Schritte und Meta
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m18s

Auf der Rezept-Detail-Seite ein neuer „Bearbeiten"-Button (Pencil-Icon)
in der Action-Bar. Klick schaltet RecipeView auf RecipeEditor um.

Im Editor:
- Titel, Beschreibung, Portionen, Vorbereitungs-/Koch-/Gesamtzeit als
  inline-Inputs.
- Zutaten: pro Zeile Menge, Einheit, Name, Notiz + Trash-Icon zum
  Entfernen. „+ Zutat hinzufügen"-Dashed-Button am Listenende.
- Schritte: nummerierte Textareas, Trash-Icon, „+ Schritt hinzufügen".
- Mengen akzeptieren Komma- oder Punkt-Dezimalen.
- Empty-Items werden beim Speichern automatisch aussortiert.

Backend:
- Neue Repo-Funktionen updateRecipeMeta(id, patch), replaceIngredients,
  replaceSteps — letztere in einer Transaction mit delete+insert und
  FTS-Refresh.
- PATCH /api/recipes/[id] akzeptiert jetzt zusätzlich description,
  servings_default, servings_unit, prep_time_min, cook_time_min,
  total_time_min, cuisine, category, ingredients[], steps[]. Vorher
  nur title/hidden_from_recent; diese beiden bleiben als
  Kurz-Fall erhalten, damit bestehende Aufrufer unverändert laufen.
- Zod-Schema mit expliziten Grenzen (max-Länge, positive Mengen).

Tests: 3 neue Cases für updateRecipeMeta, replaceIngredients (inkl.
FTS-Update), replaceSteps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-18 12:41:10 +02:00
parent ee783ff50b
commit 9a5c626890
5 changed files with 668 additions and 11 deletions

View File

@@ -155,3 +155,79 @@ export function getRecipeIdBySourceUrl(
export function deleteRecipe(db: Database.Database, id: number): void {
db.prepare('DELETE FROM recipe WHERE id = ?').run(id);
}
export type RecipeMetaPatch = {
title?: string;
description?: string | null;
servings_default?: number | null;
servings_unit?: string | null;
prep_time_min?: number | null;
cook_time_min?: number | null;
total_time_min?: number | null;
cuisine?: string | null;
category?: string | null;
};
export function updateRecipeMeta(
db: Database.Database,
id: number,
patch: RecipeMetaPatch
): void {
const fields: string[] = [];
const values: unknown[] = [];
for (const key of [
'title',
'description',
'servings_default',
'servings_unit',
'prep_time_min',
'cook_time_min',
'total_time_min',
'cuisine',
'category'
] as const) {
if (patch[key] !== undefined) {
fields.push(`${key} = ?`);
values.push(patch[key]);
}
}
if (fields.length === 0) return;
fields.push('updated_at = CURRENT_TIMESTAMP');
db.prepare(`UPDATE recipe SET ${fields.join(', ')} WHERE id = ?`).run(...values, id);
}
export function replaceIngredients(
db: Database.Database,
recipeId: number,
ingredients: Ingredient[]
): void {
const tx = db.transaction(() => {
db.prepare('DELETE FROM ingredient WHERE recipe_id = ?').run(recipeId);
const ins = db.prepare(
`INSERT INTO ingredient(recipe_id, position, quantity, unit, name, note, raw_text)
VALUES (?, ?, ?, ?, ?, ?, ?)`
);
for (const ing of ingredients) {
ins.run(recipeId, ing.position, ing.quantity, ing.unit, ing.name, ing.note, ing.raw_text);
}
refreshFts(db, recipeId);
});
tx();
}
export function replaceSteps(
db: Database.Database,
recipeId: number,
steps: Step[]
): void {
const tx = db.transaction(() => {
db.prepare('DELETE FROM step WHERE recipe_id = ?').run(recipeId);
const ins = db.prepare(
'INSERT INTO step(recipe_id, position, text) VALUES (?, ?, ?)'
);
for (const step of steps) {
ins.run(recipeId, step.position, step.text);
}
});
tx();
}