- Keine Vorschläge verfügbar. -
-diff --git a/frontend/src/lib/planner/DayMealCard.svelte b/frontend/src/lib/planner/DayMealCard.svelte index eeaed6a..fb03bdf 100644 --- a/frontend/src/lib/planner/DayMealCard.svelte +++ b/frontend/src/lib/planner/DayMealCard.svelte @@ -16,12 +16,14 @@ slot, isToday = false, isSelected = false, - readonly = false + readonly = false, + onaddrecipe }: { slot: Slot; isToday?: boolean; isSelected?: boolean; readonly?: boolean; + onaddrecipe?: () => void; } = $props(); let metadata = $derived( @@ -64,23 +66,27 @@ > Jetzt kochen - - Tauschen - + {#if onaddrecipe} + + {/if} {/if} {:else}
Kein Gericht geplant
- {#if !readonly} - + Gericht hinzufügen - + {/if} {/if} diff --git a/frontend/src/lib/planner/DayMealCard.test.ts b/frontend/src/lib/planner/DayMealCard.test.ts index dbac4db..cafa53a 100644 --- a/frontend/src/lib/planner/DayMealCard.test.ts +++ b/frontend/src/lib/planner/DayMealCard.test.ts @@ -1,5 +1,6 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; +import { userEvent } from '@testing-library/user-event'; import DayMealCard from './DayMealCard.svelte'; const slot = { @@ -14,22 +15,29 @@ describe('DayMealCard', () => { expect(screen.getByText('Pasta Bolognese')).toBeTruthy(); }); - it('shows Cook now and Tauschen links when not readonly', () => { - render(DayMealCard, { props: { slot, isToday: false, readonly: false } }); + it('shows Jetzt kochen link and Tauschen button when not readonly and onaddrecipe provided', () => { + render(DayMealCard, { props: { slot, isToday: false, readonly: false, onaddrecipe: vi.fn() } }); expect(screen.getByRole('link', { name: /Jetzt kochen/i })).toBeTruthy(); - expect(screen.getByRole('link', { name: /Tauschen/i })).toBeTruthy(); + expect(screen.getByRole('button', { name: /Tauschen/i })).toBeTruthy(); }); - it('Tauschen link navigates to suggestions for the slot day', () => { + it('Tauschen button calls onaddrecipe when clicked', async () => { + const onaddrecipe = vi.fn(); + const user = userEvent.setup(); + render(DayMealCard, { props: { slot, isToday: false, readonly: false, onaddrecipe } }); + await user.click(screen.getByRole('button', { name: /Tauschen/i })); + expect(onaddrecipe).toHaveBeenCalledOnce(); + }); + + it('hides Tauschen button when onaddrecipe not provided', () => { render(DayMealCard, { props: { slot, isToday: false, readonly: false } }); - const link = screen.getByRole('link', { name: /Tauschen/i }); - expect(link.getAttribute('href')).toContain('2026-03-30'); + expect(screen.queryByRole('button', { name: /Tauschen/i })).toBeNull(); }); it('hides action links when readonly', () => { - render(DayMealCard, { props: { slot, isToday: false, readonly: true } }); + render(DayMealCard, { props: { slot, isToday: false, readonly: true, onaddrecipe: vi.fn() } }); expect(screen.queryByRole('link', { name: /Jetzt kochen/i })).toBeNull(); - expect(screen.queryByRole('link', { name: /Tauschen/i })).toBeNull(); + expect(screen.queryByRole('button', { name: /Tauschen/i })).toBeNull(); }); it('applies today styling when isToday is true', () => { @@ -55,9 +63,22 @@ describe('DayMealCard', () => { expect(screen.getByText(/Easy/)).toBeTruthy(); }); - it('empty state shows add link with suggestions href', () => { + it('empty state shows add button when onaddrecipe provided', () => { + const onaddrecipe = vi.fn(); + render(DayMealCard, { props: { slot: { id: 's2', slotDate: '2026-03-31', recipe: null }, isToday: false, readonly: false, onaddrecipe } }); + expect(screen.getByRole('button', { name: /Gericht hinzufügen/i })).toBeTruthy(); + }); + + it('add button calls onaddrecipe when clicked', async () => { + const onaddrecipe = vi.fn(); + const user = userEvent.setup(); + render(DayMealCard, { props: { slot: { id: 's2', slotDate: '2026-03-31', recipe: null }, isToday: false, readonly: false, onaddrecipe } }); + await user.click(screen.getByRole('button', { name: /Gericht hinzufügen/i })); + expect(onaddrecipe).toHaveBeenCalledOnce(); + }); + + it('empty state hides add button when onaddrecipe not provided', () => { render(DayMealCard, { props: { slot: { id: 's2', slotDate: '2026-03-31', recipe: null }, isToday: false, readonly: false } }); - const link = screen.getByRole('link', { name: /Gericht hinzufügen/i }); - expect(link.getAttribute('href')).toContain('2026-03-31'); + expect(screen.queryByRole('button', { name: /Gericht hinzufügen/i })).toBeNull(); }); }); diff --git a/frontend/src/routes/(app)/planner/+page.svelte b/frontend/src/routes/(app)/planner/+page.svelte index 68f8d1b..aa997e7 100644 --- a/frontend/src/routes/(app)/planner/+page.svelte +++ b/frontend/src/routes/(app)/planner/+page.svelte @@ -205,6 +205,7 @@ isToday={selectedDay === today} isSelected={true} readonly={!isPlanner} + onaddrecipe={isPlanner ? () => (pickerOpen = true) : undefined} /> diff --git a/frontend/src/routes/(app)/planner/suggestions/+page.server.ts b/frontend/src/routes/(app)/planner/suggestions/+page.server.ts deleted file mode 100644 index 57e437d..0000000 --- a/frontend/src/routes/(app)/planner/suggestions/+page.server.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { PageServerLoad, Actions } from './$types'; -import { redirect } from '@sveltejs/kit'; -import { apiClient } from '$lib/server/api'; -import { getWeekStart } from '$lib/planner/week'; - -export const load: PageServerLoad = async ({ fetch, url, locals: _locals }) => { - const weekParam = url.searchParams.get('week'); - const weekStart = weekParam ?? getWeekStart(new Date()); - const selectedDay = url.searchParams.get('day') ?? weekStart; - - const api = apiClient(fetch); - - // Load the week plan for context (week-so-far display) - const { data: weekPlan, error: weekPlanError } = await api.GET('/v1/week-plans', { - params: { query: { weekStart } } - }); - - if (weekPlanError || !weekPlan?.id) { - return { weekPlan: null, suggestions: [], selectedDay, weekStart }; - } - - // Load variety-aware suggestions for the selected day - const { data: suggestionsData } = await api.GET('/v1/week-plans/{id}/suggestions', { - params: { path: { id: weekPlan.id }, query: { slotDate: selectedDay } } - }); - - // Sort by simulatedScore descending (highest = best variety fit) - const suggestions = (suggestionsData?.suggestions ?? []).sort( - (a: any, b: any) => (b.simulatedScore ?? 0) - (a.simulatedScore ?? 0) - ); - - return { weekPlan, suggestions, selectedDay, weekStart }; -}; - -export const actions: Actions = { - pickSuggestion: async ({ fetch, request, locals }) => { - // Role guard: only planners may assign meals - if (locals.benutzer?.rolle !== 'planer') { - return { success: false, error: 'Keine Berechtigung.' }; - } - - const formData = await request.formData(); - const planId = formData.get('planId') as string; - const recipeId = formData.get('recipeId') as string; - const slotDate = formData.get('slotDate') as string; - const weekStart = formData.get('weekStart') as string; - - // Validate slotDate format - if (!slotDate || !/^\d{4}-\d{2}-\d{2}$/.test(slotDate)) { - return { success: false, error: 'Ungültiges Datum.' }; - } - - // Validate planId is non-empty - if (!planId) { - return { success: false, error: 'Fehlende Plan-ID.' }; - } - - // Validate recipeId is UUID-like format - if (!recipeId || !/^[0-9a-f-]{36}$/.test(recipeId)) { - return { success: false, error: 'Ungültige Rezept-ID.' }; - } - - const api = apiClient(fetch); - const { data, error } = await api.POST('/v1/week-plans/{id}/slots', { - params: { path: { id: planId } }, - body: { slotDate, recipeId } - }); - - if (error || !data) { - return { success: false, error: 'Gericht konnte nicht hinzugefügt werden.' }; - } - - // Redirect back to the planner after successful pick (spec: "returns to C1") - redirect(303, `/planner?week=${weekStart || slotDate.slice(0, 7) + '-01'}`); - } -}; diff --git a/frontend/src/routes/(app)/planner/suggestions/+page.svelte b/frontend/src/routes/(app)/planner/suggestions/+page.svelte deleted file mode 100644 index b0e92cd..0000000 --- a/frontend/src/routes/(app)/planner/suggestions/+page.svelte +++ /dev/null @@ -1,199 +0,0 @@ - - -- Keine Vorschläge verfügbar. -
- - Gesamte Rezeptbibliothek durchsuchen → - -- Keine Vorschläge verfügbar. -
-