diff --git a/frontend/src/lib/planner/RecipePicker.svelte b/frontend/src/lib/planner/RecipePicker.svelte index 47546b0..185f2bd 100644 --- a/frontend/src/lib/planner/RecipePicker.svelte +++ b/frontend/src/lib/planner/RecipePicker.svelte @@ -8,6 +8,9 @@ suggestions = [], allRecipes = [], isLoading = false, + isDisabled = false, + excludeRecipeId, + replacingRecipe, onpick }: { planId: string; @@ -16,23 +19,32 @@ suggestions: Suggestion[]; allRecipes: Recipe[]; isLoading?: boolean; + isDisabled?: boolean; + excludeRecipeId?: string; + replacingRecipe?: { name: string; meta?: string }; onpick: (recipeId: string, recipeName: string) => void; } = $props(); let searchQuery = $state(''); let topRecommendations = $derived( - suggestions.filter((s) => s.scoreDelta > 0).slice(0, 5) + suggestions + .filter((s) => s.scoreDelta > 0 && s.recipe.id !== excludeRecipeId) + .slice(0, 5) ); let scoreMap = $derived( new Map(suggestions.map((s) => [s.recipe.id, s])) ); + let baseRecipes = $derived( + excludeRecipeId ? allRecipes.filter((r) => r.id !== excludeRecipeId) : allRecipes + ); + let filteredRecipes = $derived( searchQuery.trim() === '' - ? allRecipes - : allRecipes.filter((r) => + ? baseRecipes + : baseRecipes.filter((r) => r.name.toLowerCase().includes(searchQuery.toLowerCase()) ) ); @@ -86,6 +98,22 @@

+ + {#if replacingRecipe} +
+

+ Wird ersetzt +

+ + {replacingRecipe.name}{#if replacingRecipe.meta} · {replacingRecipe.meta}{/if} + +
+ {/if} +
onpick(suggestion.recipe.id, suggestion.recipe.name)} - style="flex-shrink: 0; font-family: var(--font-sans); font-size: 10px; font-weight: 500; padding: 4px 8px; border-radius: var(--radius-md); background: var(--green-dark); color: #fff; border: none; cursor: pointer;" + disabled={isDisabled} + style="flex-shrink: 0; font-family: var(--font-sans); font-size: 10px; font-weight: 500; padding: 4px 8px; border-radius: var(--radius-md); background: var(--green-dark); color: #fff; border: none; cursor: {isDisabled ? 'default' : 'pointer'}; opacity: {isDisabled ? '0.4' : '1'};" > + Wählen @@ -194,7 +223,8 @@ type="button" aria-label="Wählen" onclick={() => onpick(recipe.id, recipe.name)} - style="flex-shrink: 0; font-family: var(--font-sans); font-size: 10px; font-weight: 500; padding: 4px 8px; border-radius: var(--radius-md); background: var(--green-dark); color: #fff; border: none; cursor: pointer;" + disabled={isDisabled} + style="flex-shrink: 0; font-family: var(--font-sans); font-size: 10px; font-weight: 500; padding: 4px 8px; border-radius: var(--radius-md); background: var(--green-dark); color: #fff; border: none; cursor: {isDisabled ? 'default' : 'pointer'}; opacity: {isDisabled ? '0.4' : '1'};" > + Wählen diff --git a/frontend/src/lib/planner/RecipePicker.test.ts b/frontend/src/lib/planner/RecipePicker.test.ts index 3036488..1100469 100644 --- a/frontend/src/lib/planner/RecipePicker.test.ts +++ b/frontend/src/lib/planner/RecipePicker.test.ts @@ -174,4 +174,40 @@ describe('RecipePicker', () => { expect(screen.queryByTestId('suggestions-loading')).toBeNull(); expect(screen.getByText(/Empfohlen/i)).toBeTruthy(); }); + + it('shows Wird ersetzt banner when replacingRecipe is provided', () => { + render(RecipePicker, { props: { ...baseProps, replacingRecipe: { name: 'Pasta', meta: '20 Min · easy' } } }); + expect(screen.getByText(/Wird ersetzt/i)).toBeTruthy(); + expect(screen.getByTestId('replacing-name').textContent).toContain('Pasta'); + }); + + it('hides Wird ersetzt banner when replacingRecipe is not provided', () => { + render(RecipePicker, { props: baseProps }); + expect(screen.queryByText(/Wird ersetzt/i)).toBeNull(); + }); + + it('excludes recipe from Alle Rezepte when excludeRecipeId is set', () => { + render(RecipePicker, { props: { ...baseProps, excludeRecipeId: 'r2' } }); + expect(screen.queryByText('Spaghetti Carbonara')).toBeNull(); + expect(screen.getByText('Beef Bourguignon')).toBeTruthy(); + expect(screen.getByText('Tomatensuppe')).toBeTruthy(); + }); + + it('excludes recipe from Empfohlen when excludeRecipeId matches a positive-delta suggestion', () => { + // s1 (Lachsfilet, scoreDelta=1.5) would normally appear in Empfohlen + render(RecipePicker, { props: { ...baseProps, excludeRecipeId: 's1' } }); + expect(screen.queryByText('Lachsfilet')).toBeNull(); + }); + + it('disables Wählen buttons when isDisabled is true', () => { + render(RecipePicker, { props: { ...baseProps, isDisabled: true } }); + const buttons = screen.getAllByRole('button', { name: /Wählen/i }); + buttons.forEach((btn) => expect((btn as HTMLButtonElement).disabled).toBe(true)); + }); + + it('enables Wählen buttons when isDisabled is false', () => { + render(RecipePicker, { props: { ...baseProps, isDisabled: false } }); + const buttons = screen.getAllByRole('button', { name: /Wählen/i }); + buttons.forEach((btn) => expect((btn as HTMLButtonElement).disabled).toBe(false)); + }); }); diff --git a/frontend/src/routes/(app)/planner/+page.svelte b/frontend/src/routes/(app)/planner/+page.svelte index 5fa8284..0070032 100644 --- a/frontend/src/routes/(app)/planner/+page.svelte +++ b/frontend/src/routes/(app)/planner/+page.svelte @@ -66,6 +66,7 @@ const activePickerDate = $derived( pickerOpen ? selectedDay + : swapSheetOpen ? selectedDay : panelState.kind === 'recipe-picker' ? panelState.date : null ); @@ -357,18 +358,18 @@ selectedSlot.recipe?.cookTimeMin ? `${selectedSlot.recipe.cookTimeMin} Min` : null, selectedSlot.recipe?.effort ?? null ].filter(Boolean).join(' · ')} -
- (swapSheetOpen = false)} - /> -
+
@@ -607,13 +608,16 @@ pickerSlot.recipe.cookTimeMin ? `${pickerSlot.recipe.cookTimeMin} Min` : null, pickerSlot.recipe.effort ?? null ].filter(Boolean).join(' · ')} -
- +