feat: complete mealplanner app (backend + frontend + deployment)
.NET 8 backend with Zitadel JWT auth, TheMealDB integration, weekly meal plan generation, shopping list aggregation. Vue 3 + Tailwind 4 frontend with dark emerald theme, manual OIDC PKCE auth, all views implemented. Multi-stage Dockerfile with nginx reverse proxy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
232
frontend/src/stores/mealPlan.ts
Normal file
232
frontend/src/stores/mealPlan.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useApi } from '../composables/useApi'
|
||||
|
||||
export interface Ingredient {
|
||||
id?: number
|
||||
name: string
|
||||
amount: number
|
||||
unit: string
|
||||
category: string
|
||||
}
|
||||
|
||||
export interface Recipe {
|
||||
id: number
|
||||
title: string
|
||||
imageUrl?: string
|
||||
instructions?: string
|
||||
ingredients: Ingredient[]
|
||||
}
|
||||
|
||||
export interface MealPlanEntry {
|
||||
id: number
|
||||
date: string
|
||||
dayOfWeek: number
|
||||
recipeId: number
|
||||
recipe: Recipe
|
||||
}
|
||||
|
||||
export interface MealPlan {
|
||||
id: number
|
||||
weekStartDate: string
|
||||
entries: MealPlanEntry[]
|
||||
}
|
||||
|
||||
export interface ShoppingItem {
|
||||
id: number
|
||||
name: string
|
||||
amount: number
|
||||
unit: string
|
||||
category: string
|
||||
isChecked: boolean
|
||||
}
|
||||
|
||||
export const useMealPlanStore = defineStore('mealPlan', () => {
|
||||
const api = useApi()
|
||||
|
||||
const currentPlan = ref<MealPlan | null>(null)
|
||||
const loading = ref(false)
|
||||
const generating = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchCurrentPlan(): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
currentPlan.value = await api.get<MealPlan>('/mealplans/current')
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === 'HTTP 404') {
|
||||
currentPlan.value = null
|
||||
} else {
|
||||
error.value = 'Fehler beim Laden des Wochenplans.'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePlan(): Promise<void> {
|
||||
generating.value = true
|
||||
error.value = null
|
||||
try {
|
||||
currentPlan.value = await api.post<MealPlan>('/mealplans/generate')
|
||||
} catch {
|
||||
error.value = 'Fehler beim Generieren des Plans.'
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function swapMeal(entryId: number, recipeId: number): Promise<void> {
|
||||
error.value = null
|
||||
try {
|
||||
const updated = await api.put<MealPlanEntry>(`/mealplans/entries/${entryId}/swap`, { recipeId })
|
||||
if (currentPlan.value) {
|
||||
const idx = currentPlan.value.entries.findIndex(e => e.id === entryId)
|
||||
if (idx !== -1) {
|
||||
currentPlan.value.entries[idx] = updated
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
error.value = 'Fehler beim Austauschen des Rezepts.'
|
||||
}
|
||||
}
|
||||
|
||||
async function rerollMeal(entryId: number): Promise<void> {
|
||||
error.value = null
|
||||
try {
|
||||
const updated = await api.post<MealPlanEntry>(`/mealplans/entries/${entryId}/reroll`)
|
||||
if (currentPlan.value) {
|
||||
const idx = currentPlan.value.entries.findIndex(e => e.id === entryId)
|
||||
if (idx !== -1) {
|
||||
currentPlan.value.entries[idx] = updated
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
error.value = 'Fehler beim Neu-Würfeln des Rezepts.'
|
||||
}
|
||||
}
|
||||
|
||||
return { currentPlan, loading, generating, error, fetchCurrentPlan, generatePlan, swapMeal, rerollMeal }
|
||||
})
|
||||
|
||||
export const useShoppingStore = defineStore('shopping', () => {
|
||||
const api = useApi()
|
||||
|
||||
const items = ref<ShoppingItem[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchItems(mealPlanId?: number | string): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const path = mealPlanId ? `/shopping/${mealPlanId}` : '/shopping/current'
|
||||
items.value = await api.get<ShoppingItem[]>(path)
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message === 'HTTP 404') {
|
||||
items.value = []
|
||||
} else {
|
||||
error.value = 'Fehler beim Laden der Einkaufsliste.'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleItem(id: number): Promise<void> {
|
||||
const item = items.value.find(i => i.id === id)
|
||||
if (!item) return
|
||||
const prev = item.isChecked
|
||||
item.isChecked = !prev
|
||||
try {
|
||||
await api.put(`/shopping/${id}/toggle`)
|
||||
} catch {
|
||||
item.isChecked = prev
|
||||
error.value = 'Fehler beim Aktualisieren.'
|
||||
}
|
||||
}
|
||||
|
||||
const uncheckedCount = () => items.value.filter(i => !i.isChecked).length
|
||||
|
||||
return { items, loading, error, fetchItems, toggleItem, uncheckedCount }
|
||||
})
|
||||
|
||||
export const useRecipesStore = defineStore('recipes', () => {
|
||||
const api = useApi()
|
||||
|
||||
const recipes = ref<Recipe[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchRecipes(): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
recipes.value = await api.get<Recipe[]>('/recipes')
|
||||
} catch {
|
||||
error.value = 'Fehler beim Laden der Rezepte.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createRecipe(data: Omit<Recipe, 'id'>): Promise<Recipe> {
|
||||
const created = await api.post<Recipe>('/recipes', data)
|
||||
recipes.value.push(created)
|
||||
return created
|
||||
}
|
||||
|
||||
async function updateRecipe(id: number, 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> {
|
||||
await api.del(`/recipes/${id}`)
|
||||
recipes.value = recipes.value.filter(r => r.id !== id)
|
||||
}
|
||||
|
||||
return { recipes, loading, error, fetchRecipes, createRecipe, updateRecipe, deleteRecipe }
|
||||
})
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const api = useApi()
|
||||
|
||||
const householdSize = ref(2)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const saved = ref(false)
|
||||
|
||||
async function fetchSettings(): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await api.get<{ householdSize: number }>('/settings')
|
||||
householdSize.value = data.householdSize
|
||||
} catch {
|
||||
// use defaults
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(): Promise<void> {
|
||||
saving.value = true
|
||||
error.value = null
|
||||
saved.value = false
|
||||
try {
|
||||
await api.put('/settings', { householdSize: householdSize.value })
|
||||
saved.value = true
|
||||
setTimeout(() => { saved.value = false }, 2000)
|
||||
} catch {
|
||||
error.value = 'Fehler beim Speichern.'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { householdSize, loading, saving, error, saved, fetchSettings, saveSettings }
|
||||
})
|
||||
Reference in New Issue
Block a user