fix: align frontend API calls with backend routes and types

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 06:49:41 +00:00
parent f58782774b
commit 1885abee64
10 changed files with 64 additions and 75 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div
class="flex items-center gap-3 py-2 px-3 rounded-lg hover:bg-zinc-800/50 transition-colors group cursor-pointer"
@click="$emit('toggle', item.id)"
@click="$emit('toggle', item.name)"
>
<!-- Checkbox -->
<div
@@ -25,7 +25,7 @@
<span
class="text-sm tabular-nums transition-colors"
:class="item.isChecked ? 'text-zinc-700' : 'text-zinc-400'"
>{{ item.amount }} {{ item.unit }}</span>
>{{ item.totalAmount != null ? item.totalAmount : '' }} {{ item.unit ?? '' }}</span>
</div>
</template>
@@ -37,6 +37,6 @@ defineProps<{
}>()
defineEmits<{
toggle: [id: number]
toggle: [name: string]
}>()
</script>

View File

@@ -75,7 +75,7 @@ const props = defineProps<{
defineEmits<{
close: []
select: [recipeId: number]
select: [recipeId: string]
}>()
const query = ref('')

View File

@@ -11,6 +11,7 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
})
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<T>
}

View File

@@ -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<void> {
async function swapMeal(entryId: string, recipeId: string): Promise<void> {
error.value = null
try {
const updated = await api.put<MealPlanEntry>(`/mealplans/entries/${entryId}/swap`, { recipeId })
@@ -92,7 +90,7 @@ export const useMealPlanStore = defineStore('mealPlan', () => {
}
}
async function rerollMeal(entryId: number): Promise<void> {
async function rerollMeal(entryId: string): Promise<void> {
error.value = null
try {
const updated = await api.post<MealPlanEntry>(`/mealplans/entries/${entryId}/reroll`)
@@ -116,13 +114,18 @@ export const useShoppingStore = defineStore('shopping', () => {
const items = ref<ShoppingItem[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const activeMealPlanId = ref<string | null>(null)
async function fetchItems(mealPlanId?: number | string): Promise<void> {
async function fetchItems(mealPlanId?: string): Promise<void> {
loading.value = true
error.value = null
try {
const path = mealPlanId ? `/shopping/${mealPlanId}` : '/shopping/current'
items.value = await api.get<ShoppingItem[]>(path)
if (mealPlanId) {
activeMealPlanId.value = mealPlanId
items.value = await api.get<ShoppingItem[]>(`/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<void> {
const item = items.value.find(i => i.id === id)
if (!item) return
async function toggleItem(itemName: string): Promise<void> {
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<Omit<Recipe, 'id'>>): Promise<void> {
async function updateRecipe(id: string, data: Partial<Omit<Recipe, 'id'>>): Promise<void> {
const updated = await api.put<Recipe>(`/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<void> {
async function deleteRecipe(id: string): Promise<void> {
await api.del(`/recipes/${id}`)
recipes.value = recipes.value.filter(r => r.id !== id)
}

View File

@@ -271,23 +271,25 @@ import type { Recipe, Ingredient } from '../stores/mealPlan'
const store = useRecipesStore()
const selectedRecipe = ref<Recipe | null>(null)
const imgErrors = ref(new Set<number>())
const imgErrors = ref(new Set<string>())
const showForm = ref(false)
const editingId = ref<number | null>(null)
const editingId = ref<string | null>(null)
const formSaving = ref(false)
const formError = ref<string | null>(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<FormState>({
@@ -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<void> {
}
}
async function handleDelete(id: number): Promise<void> {
async function handleDelete(id: string): Promise<void> {
if (!confirm('Rezept wirklich löschen?')) return
await store.deleteRecipe(id)
}

View File

@@ -82,7 +82,7 @@
<div v-if="!collapsed.has(String(category))" class="px-2 py-1">
<ShoppingItem
v-for="item in groupItems"
:key="item.id"
:key="item.name"
:item="item"
@toggle="store.toggleItem"
/>

View File

@@ -5,7 +5,7 @@
<div>
<h1 class="text-2xl font-bold text-zinc-100">Wochenplan</h1>
<p v-if="store.currentPlan" class="text-zinc-500 text-sm mt-0.5">
{{ formatWeek(store.currentPlan.weekStartDate) }}
{{ formatWeek(store.currentPlan.weekStart) }}
</p>
</div>
<button
@@ -62,7 +62,7 @@
v-for="entry in sortedEntries"
:key="entry.id"
:entry="entry"
:day-name="getDayName(entry.dayOfWeek)"
:day-name="getDayName(entry.date)"
@swap="openSwapModal(entry)"
@reroll="handleReroll(entry)"
/>
@@ -121,12 +121,13 @@ const swapEntry = ref<MealPlanEntry | null>(null)
const DAY_NAMES = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']
function getDayName(dayOfWeek: number): string {
return DAY_NAMES[dayOfWeek] ?? `Tag ${dayOfWeek}`
function getDayName(dateStr: string): string {
const d = new Date(dateStr + 'T00:00:00')
return DAY_NAMES[d.getDay()] ?? dateStr
}
const sortedEntries = computed(() => {
return [...(store.currentPlan?.entries ?? [])].sort((a, b) => a.dayOfWeek - b.dayOfWeek)
return [...(store.currentPlan?.entries ?? [])].sort((a, b) => a.date.localeCompare(b.date))
})
function formatWeek(dateStr: string): string {
@@ -148,7 +149,7 @@ function openSwapModal(entry: MealPlanEntry): void {
}
}
async function handleSwapSelect(recipeId: number): Promise<void> {
async function handleSwapSelect(recipeId: string): Promise<void> {
if (!swapEntry.value) return
await store.swapMeal(swapEntry.value.id, recipeId)
swapEntry.value = null