From 8234c2f162bcadf3bd4741abfbb3c75a5e350f3b Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 9 Apr 2026 11:38:47 +0200 Subject: [PATCH] feat(planner): RecipePicker uses scoreDelta/hasConflict, drop currentVarietyScore, add isLoading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Suggestion interface: { recipe, scoreDelta, hasConflict } (no simulatedScore) - Badge renders from hasConflict directly — no client-side delta computation needed - New isLoading prop shows skeleton rows while suggestions fetch is in flight - currentVarietyScore prop removed from component and both call sites follow in next commit Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/planner/RecipePicker.svelte | 34 +++++++++++++++---- frontend/src/lib/planner/RecipePicker.test.ts | 25 ++++++++++---- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/planner/RecipePicker.svelte b/frontend/src/lib/planner/RecipePicker.svelte index 1d2ffde..51940ac 100644 --- a/frontend/src/lib/planner/RecipePicker.svelte +++ b/frontend/src/lib/planner/RecipePicker.svelte @@ -8,24 +8,25 @@ interface Suggestion { recipe: Recipe; - simulatedScore: number; + scoreDelta: number; + hasConflict: boolean; } let { planId, date, dateLabel, - currentVarietyScore = 0, suggestions = [], allRecipes = [], + isLoading = false, onpick }: { planId: string; date: string; dateLabel: string; - currentVarietyScore?: number; suggestions: Suggestion[]; allRecipes: Recipe[]; + isLoading?: boolean; onpick: (recipeId: string, recipeName: string) => void; } = $props(); @@ -71,7 +72,27 @@ - {#if suggestions.length > 0} + {#if isLoading} +
+ {#each [1, 2, 3] as i (i)} +
+
+
+
+
+
+
+ {/each} +
+ {:else if suggestions.length > 0}
@@ -79,7 +100,6 @@
{#each suggestions as suggestion (suggestion.recipe.id)} - {@const delta = suggestion.simulatedScore - currentVarietyScore} {@const meta = recipeMetadata(suggestion.recipe)}
{/if} - {#if delta > 0} + {#if !suggestion.hasConflict} - ↑ +{delta.toFixed(0)} Punkte + ↑ +{suggestion.scoreDelta.toFixed(0)} Punkte {:else} { expect(screen.getByText('Hähnchen-Curry')).toBeTruthy(); }); - it('shows green badge for suggestions with positive delta', () => { + it('shows green badge when hasConflict is false', () => { render(RecipePicker, { props: baseProps }); - // Lachsfilet: simulatedScore 9.5 - currentVarietyScore 7.5 = +2 → green badge + // Lachsfilet: hasConflict = false → green badge const badge = screen.getByTestId('badge-s1'); expect(badge.getAttribute('data-type')).toBe('good'); }); - it('shows yellow badge for suggestions with zero or negative delta', () => { + it('shows yellow badge when hasConflict is true', () => { render(RecipePicker, { props: baseProps }); - // Hähnchen-Curry: 6.0 - 7.5 = -1.5 → yellow badge + // Hähnchen-Curry: hasConflict = true → yellow badge const badge = screen.getByTestId('badge-s2'); expect(badge.getAttribute('data-type')).toBe('warning'); }); @@ -98,4 +97,16 @@ describe('RecipePicker', () => { await userEvent.type(input, 'xyznotfound'); expect(screen.getByText(/Keine Treffer/i)).toBeTruthy(); }); + + it('shows loading skeleton instead of Empfohlen section when isLoading is true', () => { + render(RecipePicker, { props: { ...baseProps, isLoading: true } }); + expect(screen.getByTestId('suggestions-loading')).toBeTruthy(); + expect(screen.queryByText(/Empfohlen/i)).toBeNull(); + }); + + it('hides loading skeleton when isLoading is false and suggestions are present', () => { + render(RecipePicker, { props: { ...baseProps, isLoading: false } }); + expect(screen.queryByTestId('suggestions-loading')).toBeNull(); + expect(screen.getByText(/Empfohlen/i)).toBeTruthy(); + }); });