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:
@@ -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>
|
||||
|
||||
@@ -75,7 +75,7 @@ const props = defineProps<{
|
||||
|
||||
defineEmits<{
|
||||
close: []
|
||||
select: [recipeId: number]
|
||||
select: [recipeId: string]
|
||||
}>()
|
||||
|
||||
const query = ref('')
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user