refactor(variety): fix \$derived.by pattern, remove dead import, use pure functions

- Change all \$derived(() => {...}) to \$derived.by(() => {...}) — values not functions
- Remove unused formatDayLabel import
- Delegate subScores to computeSubScores(), warnings to computeWarnings()
- Remove () call syntax from all template reactive references

Addresses Kai blockers: anti-pattern derived, dead import.
Addresses QA blocker: logic now exercised by unit tests in variety.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 11:36:00 +02:00
parent 9adf786b8f
commit cb15143c30

View File

@@ -3,7 +3,7 @@
import ScoreBreakdownList from '$lib/planner/ScoreBreakdownList.svelte';
import VarietyWarningCards from '$lib/planner/VarietyWarningCards.svelte';
import EffortBar from '$lib/planner/EffortBar.svelte';
import { formatDayLabel } from '$lib/planner/week';
import { computeSubScores, computeWarnings } from '$lib/planner/variety';
let { data } = $props();
@@ -14,7 +14,7 @@
let score = $derived(varietyScore?.score ?? 0);
// Derive effort distribution from week plan slots
let effortCounts = $derived(() => {
let effortCounts = $derived.by(() => {
const slots = weekPlan?.slots ?? [];
let easy = 0, medium = 0, hard = 0;
for (const slot of slots) {
@@ -28,65 +28,21 @@
// Derive sub-scores from API data
// TODO: replace with API-provided sub-scores once backend supports them.
let subScores = $derived(() => {
const proteinRepeats = (varietyScore?.tagRepeats ?? []).filter(
(t: any) => t.tagType === 'protein'
).length;
const ingredientOverlapCount = (varietyScore?.ingredientOverlaps ?? []).length;
const { easy, medium, hard } = effortCounts();
const total = easy + medium + hard;
// Effort balance: ideal is roughly equal split; penalise extreme distributions
const effortBalance =
total === 0
? 10
: Math.round(Math.max(0, 10 - Math.abs(easy - hard) * 1.5));
return {
proteinDiversity: Math.max(0, Math.round(10 - proteinRepeats * 2)),
ingredientOverlap: Math.max(0, Math.round(10 - ingredientOverlapCount * 1.5)),
effortBalance: Math.min(10, effortBalance)
};
});
let subScores = $derived.by(() => computeSubScores({
tagRepeats: varietyScore?.tagRepeats ?? [],
ingredientOverlaps: varietyScore?.ingredientOverlaps ?? [],
...effortCounts
}));
// Build warning list from API data
let warnings = $derived(() => {
const result: { title: string; explanation: string }[] = [];
// Protein repeats
for (const repeat of varietyScore?.tagRepeats ?? []) {
if ((repeat.days?.length ?? 0) > 1) {
const days = (repeat.days ?? []).map((d: string) => d).join(', ');
result.push({
title: `${repeat.tagName} mehrfach diese Woche`,
explanation: `${days} — erwäge einen Tausch für mehr Protein-Abwechslung.`
});
}
}
// Ingredient overlaps
for (const overlap of varietyScore?.ingredientOverlaps ?? []) {
if ((overlap.days?.length ?? 0) > 1) {
const days = (overlap.days ?? []).join(', ');
result.push({
title: `${overlap.ingredientName} in mehreren Gerichten`,
explanation: `${days} — sorge für Zutaten-Abwechslung.`
});
}
}
// Duplicate recipes in plan
for (const name of varietyScore?.duplicatesInPlan ?? []) {
result.push({
title: `${name} doppelt geplant`,
explanation: 'Dasselbe Rezept erscheint mehrfach — tausche eines aus.'
});
}
return result;
});
let warnings = $derived.by(() => computeWarnings({
tagRepeats: varietyScore?.tagRepeats ?? [],
ingredientOverlaps: varietyScore?.ingredientOverlaps ?? [],
duplicatesInPlan: varietyScore?.duplicatesInPlan ?? []
}));
// Protein grid: map protein tags to days of the week
let proteinByDay = $derived(() => {
let proteinByDay = $derived.by(() => {
const map: Record<string, string> = {};
for (const repeat of varietyScore?.tagRepeats ?? []) {
if (repeat.tagType === 'protein') {
@@ -147,16 +103,16 @@
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Bewertung im Detail
</h2>
<ScoreBreakdownList subScores={subScores()} />
<ScoreBreakdownList {subScores} />
</div>
<!-- Warnings -->
{#if warnings().length > 0}
{#if warnings.length > 0}
<div class="space-y-3">
<h2 class="font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Hinweise
</h2>
<VarietyWarningCards warnings={warnings()} />
<VarietyWarningCards {warnings} />
</div>
{/if}
{/if}
@@ -208,7 +164,7 @@
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Bewertung im Detail
</h2>
<ScoreBreakdownList subScores={subScores()} />
<ScoreBreakdownList {subScores} />
</div>
</div>
@@ -220,10 +176,10 @@
Protein-Verteilung
</h2>
<div class="grid grid-cols-7 gap-[6px]">
{#each weekDayAbbrs as abbr, i}
{#each weekDayAbbrs as abbr, i (weekDayKeys[i])}
{@const key = weekDayKeys[i]}
{@const protein = proteinByDay()[key]}
{@const isRepeated = protein && Object.values(proteinByDay()).filter((p) => p === protein).length > 1}
{@const protein = proteinByDay[key]}
{@const isRepeated = protein && Object.values(proteinByDay).filter((p) => p === protein).length > 1}
<div class="flex flex-col items-center gap-1">
<span class="font-[var(--font-sans)] text-[10px] text-[var(--color-text-muted)]">{abbr}</span>
<div
@@ -247,11 +203,11 @@
<h2 class="mb-3 font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Aufwandsverteilung
</h2>
{#if (effortCounts().easy + effortCounts().medium + effortCounts().hard) > 0}
{#if (effortCounts.easy + effortCounts.medium + effortCounts.hard) > 0}
<EffortBar
easy={effortCounts().easy}
medium={effortCounts().medium}
hard={effortCounts().hard}
easy={effortCounts.easy}
medium={effortCounts.medium}
hard={effortCounts.hard}
/>
{:else}
<p class="font-[var(--font-sans)] text-[12px] text-[var(--color-text-muted)]">
@@ -263,12 +219,12 @@
</div>
<!-- Bottom: warnings, full width -->
{#if warnings().length > 0}
{#if warnings.length > 0}
<div class="mt-8 space-y-3">
<h2 class="font-[var(--font-sans)] text-[12px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
Hinweise
</h2>
<VarietyWarningCards warnings={warnings()} />
<VarietyWarningCards {warnings} />
</div>
{/if}
{/if}