From a1f8fe228c3f8a6f79512ed1180bab149458f8df Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Fri, 10 Apr 2026 17:45:24 +0200 Subject: [PATCH] Add update functionality for recipes and create edit page --- backend/src/recipes/recipes.controller.ts | 10 +- backend/src/recipes/recipes.service.ts | 34 +++ frontend/app/recipes/RecipePreview.tsx | 61 +++++- frontend/app/recipes/[id]/edit/page.tsx | 251 ++++++++++++++++++++++ 4 files changed, 354 insertions(+), 2 deletions(-) create mode 100644 frontend/app/recipes/[id]/edit/page.tsx diff --git a/backend/src/recipes/recipes.controller.ts b/backend/src/recipes/recipes.controller.ts index b951e56c..26498cb5 100644 --- a/backend/src/recipes/recipes.controller.ts +++ b/backend/src/recipes/recipes.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, ParseIntPipe, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, ParseIntPipe, Post, Patch } from '@nestjs/common'; import { RecipesService } from './recipes.service'; import { CreateRecipeDto } from './dto/create-recipe.dto'; @@ -25,4 +25,12 @@ export class RecipesController { async create(@Body() createRecipeDto: CreateRecipeDto) { return this.recipesService.create(createRecipeDto); } + + @Patch(':id') + async update( + @Param('id', ParseIntPipe) id: number, + @Body() createRecipeDto: CreateRecipeDto, + ) { + return this.recipesService.update(id, createRecipeDto); + } } \ No newline at end of file diff --git a/backend/src/recipes/recipes.service.ts b/backend/src/recipes/recipes.service.ts index fd9efb48..299ce0a7 100644 --- a/backend/src/recipes/recipes.service.ts +++ b/backend/src/recipes/recipes.service.ts @@ -302,6 +302,40 @@ export class RecipesService { return recipe; } + async update(id: number, updateRecipeDto: CreateRecipeDto) { + // Först, ta bort gamla ingredienser + await this.prisma.recipeIngredient.deleteMany({ + where: { recipeId: id }, + }); + + // Uppdatera receptet och lägg till nya ingredienser + const recipe = await this.prisma.recipe.update({ + where: { id }, + data: { + name: updateRecipeDto.name, + description: updateRecipeDto.description || null, + instructions: updateRecipeDto.instructions || null, + ingredients: { + create: updateRecipeDto.ingredients.map((ingredient) => ({ + productId: ingredient.productId, + quantity: ingredient.quantity.toString(), + unit: ingredient.unit, + note: ingredient.note || null, + })), + }, + }, + include: { + ingredients: { + include: { + product: true, + }, + }, + }, + }); + + return recipe; + } + async create(createRecipeDto: CreateRecipeDto) { const recipe = await this.prisma.recipe.create({ data: { diff --git a/frontend/app/recipes/RecipePreview.tsx b/frontend/app/recipes/RecipePreview.tsx index 65529140..46ce4579 100644 --- a/frontend/app/recipes/RecipePreview.tsx +++ b/frontend/app/recipes/RecipePreview.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useTransition } from 'react'; +import Link from 'next/link'; import type { Recipe, RecipeInventoryPreview, @@ -115,7 +116,7 @@ export default function RecipePreview({ recipes }: Props) { -
+
+ {selectedRecipeId && ( + + Redigera recept + + )}
{error ?

{error}

: null} @@ -154,6 +172,31 @@ export default function RecipePreview({ recipes }: Props) { : 'Kan inte lagas exakt ännu'}
+ + {preview.summary.unitMismatchCount > 0 && ( +
+ ⚠️ Enhetskonflikt! {preview.summary.unitMismatchCount} ingrediens + {preview.summary.unitMismatchCount !== 1 ? 'er har' : ' har'} olika enheter än vad som finns i hemmavaror. +
+ + T.ex. receptet säger "0.5 st" men du har lagrat "1.3 kg". Du kan antingen: +
    +
  • Redigera receptet för att matcha dina enheter
  • +
  • Lagra ingrediensen med samma enhet som receptet använder
  • +
+
+
+ )}
@@ -215,6 +258,22 @@ export default function RecipePreview({ recipes }: Props) { Saknas: {ingredient.missingQuantity} {ingredient.requiredUnit}
) : null} + + {ingredient.status === 'unit_mismatch' ? ( +
+ Enhetsproblem: Receptet kräver {ingredient.requiredUnit} men hemmavaror lagras i andra enheter. + Uppdatera receptet eller lagra ingrediensen med rätt enhet. +
+ ) : null} {ingredient.matchingInventoryItems.length > 0 ? ( diff --git a/frontend/app/recipes/[id]/edit/page.tsx b/frontend/app/recipes/[id]/edit/page.tsx new file mode 100644 index 00000000..088a4f4a --- /dev/null +++ b/frontend/app/recipes/[id]/edit/page.tsx @@ -0,0 +1,251 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import { fetchJson } from '../../../../lib/api'; +import type { Product, Recipe } from '../../../../features/inventory/types'; + +export default function EditRecipePage() { + const router = useRouter(); + const params = useParams(); + const recipeId = Array.isArray(params.id) ? params.id[0] : params.id; + + const [recipe, setRecipe] = useState({ + name: '', + description: '', + instructions: '', + ingredients: [{ productId: 0, quantity: '', unit: '', note: '', location: '' }], + }); + const [products, setProducts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const loadData = async () => { + try { + // Ladda produkter + const productsData = await fetchJson('/api/products'); + setProducts(productsData); + + // Ladda receptet + const recipeData = await fetchJson(`/api/recipes/${recipeId}`); + setRecipe({ + name: recipeData.name, + description: recipeData.description || '', + instructions: recipeData.instructions || '', + ingredients: recipeData.ingredients.map((ing: any) => ({ + productId: ing.productId, + quantity: ing.quantity.toString(), + unit: ing.unit, + note: ing.note || '', + location: ing.location || '', + })), + }); + } catch (err) { + setError((err as Error).message); + } finally { + setIsLoading(false); + } + }; + + loadData(); + }, [recipeId]); + + const handleIngredientChange = (index: number, field: string, value: string | number) => { + const newIngredients = [...recipe.ingredients]; + newIngredients[index] = { ...newIngredients[index], [field]: value }; + setRecipe({ ...recipe, ingredients: newIngredients }); + }; + + const addIngredient = () => { + setRecipe({ + ...recipe, + ingredients: [...recipe.ingredients, { productId: 0, quantity: '', unit: '', note: '', location: '' }], + }); + }; + + const removeIngredient = (index: number) => { + const newIngredients = [...recipe.ingredients]; + newIngredients.splice(index, 1); + setRecipe({ ...recipe, ingredients: newIngredients }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSaving(true); + setError(null); + + // Konvertera quantity till number för varje ingrediens + const recipeToSend = { + ...recipe, + ingredients: recipe.ingredients.map((ing) => ({ + ...ing, + quantity: Number(ing.quantity), + })), + }; + + try { + const response = await fetch(`/api/recipes/${recipeId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(recipeToSend), + }); + + if (!response.ok) { + throw new Error('Kunde inte uppdatera receptet'); + } + + router.push('/recipes'); + } catch (err) { + setError((err as Error).message); + } finally { + setIsSaving(false); + } + }; + + const UNIT_OPTIONS = [ + { value: '', label: 'Välj enhet' }, + { value: 'g', label: 'g (gram)' }, + { value: 'kg', label: 'kg (kilogram)' }, + { value: 'hg', label: 'hg (hektogram)' }, + { value: 'ml', label: 'ml (milliliter)' }, + { value: 'dl', label: 'dl (deciliter)' }, + { value: 'l', label: 'l (liter)' }, + { value: 'st', label: 'st (styck)' }, + { value: 'tsk', label: 'tsk (tesked)' }, + { value: 'msk', label: 'msk (matsked)' }, + ]; + + const LOCATION_OPTIONS = [ + { value: '', label: 'Välj plats' }, + { value: 'Kyl', label: 'Kyl' }, + { value: 'Frys', label: 'Frys' }, + { value: 'Skafferi', label: 'Skafferi' }, + { value: 'Annat', label: 'Annat' }, + ]; + + if (isLoading) { + return ( +
+

Laddar recept...

+
+ ); + } + + return ( +
+

Redigera recept

+ + {error &&

{error}

} + +
+
+ + +