From 1885abee646befd020ddd569b6ba022818c167cc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 06:49:41 +0000 Subject: [PATCH] fix: align frontend API calls with backend routes and types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: rename routes /mealplan→/mealplans, /shoppinglist→/shopping - Backend: simplify swap/reroll to entry-centric endpoints (by entryId) - Frontend: fix all interfaces to use string GUIDs instead of numbers - Frontend: fix field names (weekStart, date, totalAmount) to match backend JSON - Frontend: shopping toggle by itemName instead of non-existent id - Frontend: handle 204 No Content on DELETE responses - Docker-compose: use env vars for DB credentials Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/Controllers/MealPlanController.cs | 31 +++-------- backend/Controllers/ShoppingListController.cs | 2 +- docker-compose.yml | 5 +- frontend/src/components/ShoppingItem.vue | 6 +- frontend/src/components/SwapModal.vue | 2 +- frontend/src/composables/useApi.ts | 1 + frontend/src/stores/mealPlan.ts | 55 ++++++++++--------- frontend/src/views/RecipesView.vue | 22 ++++---- frontend/src/views/ShoppingListView.vue | 2 +- frontend/src/views/WeekPlanView.vue | 13 +++-- 10 files changed, 64 insertions(+), 75 deletions(-) diff --git a/backend/Controllers/MealPlanController.cs b/backend/Controllers/MealPlanController.cs index 83442a6..5b00771 100644 --- a/backend/Controllers/MealPlanController.cs +++ b/backend/Controllers/MealPlanController.cs @@ -5,7 +5,7 @@ using MealPlanner.Services; namespace MealPlanner.Controllers; [ApiController] -[Route("api/mealplan")] +[Route("api/mealplans")] [Authorize] public class MealPlanController(MealPlanService mealPlanService) : ControllerBase { @@ -49,21 +49,12 @@ public class MealPlanController(MealPlanService mealPlanService) : ControllerBas return Ok(plan); } - [HttpPut("{id:guid}/entry/{date}")] - public async Task SwapEntry(Guid id, string date, [FromBody] SwapRequest body) + [HttpPut("entries/{entryId:guid}/swap")] + public async Task SwapEntry(Guid entryId, [FromBody] SwapRequest body) { - if (!DateOnly.TryParse(date, out var parsedDate)) return BadRequest("Invalid date format."); - - // Find entry by plan id + date - var plan = await mealPlanService.GetPlanAsync(UserId, MealPlanService.GetWeekStart(parsedDate)); - if (plan is null || plan.Id != id) return NotFound(); - - var entry = plan.Entries.FirstOrDefault(e => e.Date == parsedDate); - if (entry is null) return NotFound(); - try { - var updated = await mealPlanService.SwapEntryAsync(entry.Id, body.RecipeId, UserId); + var updated = await mealPlanService.SwapEntryAsync(entryId, body.RecipeId, UserId); if (updated is null) return NotFound(); return Ok(updated); } @@ -73,20 +64,12 @@ public class MealPlanController(MealPlanService mealPlanService) : ControllerBas } } - [HttpPost("{id:guid}/entry/{date}/reroll")] - public async Task RerollEntry(Guid id, string date) + [HttpPost("entries/{entryId:guid}/reroll")] + public async Task RerollEntry(Guid entryId) { - if (!DateOnly.TryParse(date, out var parsedDate)) return BadRequest("Invalid date format."); - - var plan = await mealPlanService.GetPlanAsync(UserId, MealPlanService.GetWeekStart(parsedDate)); - if (plan is null || plan.Id != id) return NotFound(); - - var entry = plan.Entries.FirstOrDefault(e => e.Date == parsedDate); - if (entry is null) return NotFound(); - try { - var updated = await mealPlanService.RerollEntryAsync(entry.Id, UserId); + var updated = await mealPlanService.RerollEntryAsync(entryId, UserId); if (updated is null) return StatusCode(503, "Could not fetch a new recipe. Try again."); return Ok(updated); } diff --git a/backend/Controllers/ShoppingListController.cs b/backend/Controllers/ShoppingListController.cs index 99974a8..8594f95 100644 --- a/backend/Controllers/ShoppingListController.cs +++ b/backend/Controllers/ShoppingListController.cs @@ -5,7 +5,7 @@ using MealPlanner.Services; namespace MealPlanner.Controllers; [ApiController] -[Route("api/shoppinglist")] +[Route("api/shopping")] [Authorize] public class ShoppingListController(ShoppingListService shoppingListService) : ControllerBase { diff --git a/docker-compose.yml b/docker-compose.yml index 5ee8bbc..deafdd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,12 @@ -version: '3.8' services: app: build: . ports: - "3000:80" environment: - - ConnectionStrings__DefaultConnection=Host=host.docker.internal;Port=54320;Database=mealplanner;Username=mika;Password=${SHARED_POSTGRES_PASSWORD} + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-host.docker.internal};Port=${DB_PORT:-5432};Database=mealplanner;Username=${DB_USER};Password=${DB_PASSWORD} - Zitadel__Issuer=https://auth.kuns.dev - Zitadel__ClientId=${ZITADEL_CLIENT_ID} - - AllowedOrigin=http://localhost:3000 + - AllowedOrigin=${ALLOWED_ORIGIN:-https://essen.kuns.dev} - ASPNETCORE_URLS=http://+:5000 - ASPNETCORE_ENVIRONMENT=Production diff --git a/frontend/src/components/ShoppingItem.vue b/frontend/src/components/ShoppingItem.vue index fde40bc..b05b090 100644 --- a/frontend/src/components/ShoppingItem.vue +++ b/frontend/src/components/ShoppingItem.vue @@ -1,7 +1,7 @@ @@ -37,6 +37,6 @@ defineProps<{ }>() defineEmits<{ - toggle: [id: number] + toggle: [name: string] }>() diff --git a/frontend/src/components/SwapModal.vue b/frontend/src/components/SwapModal.vue index b4d7123..16b5819 100644 --- a/frontend/src/components/SwapModal.vue +++ b/frontend/src/components/SwapModal.vue @@ -75,7 +75,7 @@ const props = defineProps<{ defineEmits<{ close: [] - select: [recipeId: number] + select: [recipeId: string] }>() const query = ref('') diff --git a/frontend/src/composables/useApi.ts b/frontend/src/composables/useApi.ts index 3272caf..7a7db2d 100644 --- a/frontend/src/composables/useApi.ts +++ b/frontend/src/composables/useApi.ts @@ -11,6 +11,7 @@ async function request(path: string, options: RequestInit = {}): Promise { }) if (res.status === 401) { login(window.location.pathname); throw new Error('Unauthorized') } if (!res.ok) throw new Error(`HTTP ${res.status}`) + if (res.status === 204) return undefined as T return res.json() as Promise } diff --git a/frontend/src/stores/mealPlan.ts b/frontend/src/stores/mealPlan.ts index f9a01ca..63b59e4 100644 --- a/frontend/src/stores/mealPlan.ts +++ b/frontend/src/stores/mealPlan.ts @@ -3,15 +3,15 @@ import { ref } from 'vue' import { useApi } from '../composables/useApi' export interface Ingredient { - id?: number + id?: string name: string - amount: number - unit: string - category: string + amount: number | null + unit: string | null + category: string | null } export interface Recipe { - id: number + id: string title: string imageUrl?: string instructions?: string @@ -19,25 +19,23 @@ export interface Recipe { } export interface MealPlanEntry { - id: number + id: string date: string - dayOfWeek: number - recipeId: number + recipeId: string recipe: Recipe } export interface MealPlan { - id: number - weekStartDate: string + id: string + weekStart: string entries: MealPlanEntry[] } export interface ShoppingItem { - id: number name: string - amount: number - unit: string - category: string + totalAmount: number | null + unit: string | null + category: string | null isChecked: boolean } @@ -77,7 +75,7 @@ export const useMealPlanStore = defineStore('mealPlan', () => { } } - async function swapMeal(entryId: number, recipeId: number): Promise { + async function swapMeal(entryId: string, recipeId: string): Promise { error.value = null try { const updated = await api.put(`/mealplans/entries/${entryId}/swap`, { recipeId }) @@ -92,7 +90,7 @@ export const useMealPlanStore = defineStore('mealPlan', () => { } } - async function rerollMeal(entryId: number): Promise { + async function rerollMeal(entryId: string): Promise { error.value = null try { const updated = await api.post(`/mealplans/entries/${entryId}/reroll`) @@ -116,13 +114,18 @@ export const useShoppingStore = defineStore('shopping', () => { const items = ref([]) const loading = ref(false) const error = ref(null) + const activeMealPlanId = ref(null) - async function fetchItems(mealPlanId?: number | string): Promise { + async function fetchItems(mealPlanId?: string): Promise { loading.value = true error.value = null try { - const path = mealPlanId ? `/shopping/${mealPlanId}` : '/shopping/current' - items.value = await api.get(path) + if (mealPlanId) { + activeMealPlanId.value = mealPlanId + items.value = await api.get(`/shopping/${mealPlanId}`) + } else { + items.value = [] + } } catch (e) { if (e instanceof Error && e.message === 'HTTP 404') { items.value = [] @@ -134,13 +137,13 @@ export const useShoppingStore = defineStore('shopping', () => { } } - async function toggleItem(id: number): Promise { - const item = items.value.find(i => i.id === id) - if (!item) return + async function toggleItem(itemName: string): Promise { + const item = items.value.find(i => i.name === itemName) + if (!item || !activeMealPlanId.value) return const prev = item.isChecked item.isChecked = !prev try { - await api.put(`/shopping/${id}/toggle`) + await api.put(`/shopping/${activeMealPlanId.value}/check/${encodeURIComponent(itemName)}`) } catch { item.isChecked = prev error.value = 'Fehler beim Aktualisieren.' @@ -149,7 +152,7 @@ export const useShoppingStore = defineStore('shopping', () => { const uncheckedCount = () => items.value.filter(i => !i.isChecked).length - return { items, loading, error, fetchItems, toggleItem, uncheckedCount } + return { items, loading, error, activeMealPlanId, fetchItems, toggleItem, uncheckedCount } }) export const useRecipesStore = defineStore('recipes', () => { @@ -177,13 +180,13 @@ export const useRecipesStore = defineStore('recipes', () => { return created } - async function updateRecipe(id: number, data: Partial>): Promise { + async function updateRecipe(id: string, data: Partial>): Promise { const updated = await api.put(`/recipes/${id}`, data) const idx = recipes.value.findIndex(r => r.id === id) if (idx !== -1) recipes.value[idx] = updated } - async function deleteRecipe(id: number): Promise { + async function deleteRecipe(id: string): Promise { await api.del(`/recipes/${id}`) recipes.value = recipes.value.filter(r => r.id !== id) } diff --git a/frontend/src/views/RecipesView.vue b/frontend/src/views/RecipesView.vue index daabf9d..b1a11c4 100644 --- a/frontend/src/views/RecipesView.vue +++ b/frontend/src/views/RecipesView.vue @@ -271,23 +271,25 @@ import type { Recipe, Ingredient } from '../stores/mealPlan' const store = useRecipesStore() const selectedRecipe = ref(null) -const imgErrors = ref(new Set()) +const imgErrors = ref(new Set()) const showForm = ref(false) -const editingId = ref(null) +const editingId = ref(null) const formSaving = ref(false) const formError = ref(null) +interface FormIngredient { + name: string + amount: number | null + unit: string | null + category: string | null +} + interface FormState { title: string imageUrl: string instructions: string - ingredients: Array<{ - name: string - amount: number - unit: string - category: string - }> + ingredients: FormIngredient[] } const form = reactive({ @@ -328,7 +330,7 @@ function closeForm(): void { } function addIngredient(): void { - form.ingredients.push({ name: '', amount: 1, unit: 'g', category: 'Sonstiges' }) + form.ingredients.push({ name: '', amount: 1, unit: 'g', category: 'Sonstiges' } as FormIngredient) } function removeIngredient(idx: number): void { @@ -358,7 +360,7 @@ async function handleSubmit(): Promise { } } -async function handleDelete(id: number): Promise { +async function handleDelete(id: string): Promise { if (!confirm('Rezept wirklich löschen?')) return await store.deleteRecipe(id) } diff --git a/frontend/src/views/ShoppingListView.vue b/frontend/src/views/ShoppingListView.vue index 62f0cce..03f5b04 100644 --- a/frontend/src/views/ShoppingListView.vue +++ b/frontend/src/views/ShoppingListView.vue @@ -82,7 +82,7 @@
diff --git a/frontend/src/views/WeekPlanView.vue b/frontend/src/views/WeekPlanView.vue index f9e2ded..5d98da3 100644 --- a/frontend/src/views/WeekPlanView.vue +++ b/frontend/src/views/WeekPlanView.vue @@ -5,7 +5,7 @@

Wochenplan

- {{ formatWeek(store.currentPlan.weekStartDate) }} + {{ formatWeek(store.currentPlan.weekStart) }}