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(' · ')}
-
-
+