feat(veckoplan): fyll alla maltider, variation, ratt maltidstyp + plan-atgarder
Veckoplanen fyller nu ALLA slots (stegvis fallback, inga tomma dagar), undviker tva soppor/samma kok i rad och styr soppor mot lunch (inte middag). Default lunch+middag varje dag. Plan-atgarder: 'Jag lagade detta' bara ratt dag; 'Hoppa over' ersatt med 'Ta bort' (ny DELETE /v1/week-plans/:id/entries/:entryId). Verifierat mot 393 recept: 14/14 slots fyllda, 0 upprepningar, 0 soppor pa middag.
This commit is contained in:
@@ -225,6 +225,13 @@ export async function processGenerateWeekPlan(
|
||||
let candidateIndex = 0;
|
||||
const entries: Array<typeof schema.weekPlanEntries.$inferInsert> = [];
|
||||
|
||||
// Variationshjälpare: undvik två soppor / samma kök i rad, och styr soppor
|
||||
// mot lunch snarare än middag.
|
||||
const isSoup = (r: { titleSv: string }) => /sopp|soup/i.test(r.titleSv);
|
||||
const signatureOf = (r: { cuisine: string; titleSv: string }) =>
|
||||
`${r.cuisine}|${isSoup(r) ? "soup" : "dish"}`;
|
||||
let lastSignature: string | null = null;
|
||||
|
||||
for (let day = 0; day < days; day++) {
|
||||
const date = new Date(Date.parse(data.input.weekStartDate) + day * 86_400_000)
|
||||
.toISOString()
|
||||
@@ -246,21 +253,38 @@ export async function processGenerateWeekPlan(
|
||||
sortOrder: entries.length,
|
||||
});
|
||||
boxIndex++;
|
||||
lastSignature = null;
|
||||
continue;
|
||||
}
|
||||
// Nästa bästa recept som passar måltidstyp och variationsregeln
|
||||
let chosen = null;
|
||||
for (let i = 0; i < scored.length; i++) {
|
||||
const idx = (candidateIndex + i) % scored.length;
|
||||
const candidate = scored[idx]!;
|
||||
if (!candidate.recipe.mealTypes.includes(mealType)) continue;
|
||||
const used = recipeUseCount.get(candidate.recipe.id) ?? 0;
|
||||
if (used >= repeatLimit) continue;
|
||||
chosen = candidate;
|
||||
candidateIndex = idx + 1;
|
||||
break;
|
||||
}
|
||||
if (!chosen) continue;
|
||||
// Nästa bästa recept: passa måltidstyp, undvik upprepning i rad och
|
||||
// soppa på middag. Lätta reglerna stegvis så en slot ALLTID fylls om
|
||||
// det finns någon rätt av rätt måltidstyp (inga tomma dagar).
|
||||
const pick = (o: {
|
||||
avoidSig: string | null;
|
||||
noSoupAtDinner: boolean;
|
||||
respectRepeat: boolean;
|
||||
}): { c: (typeof scored)[number]; idx: number } | null => {
|
||||
for (let i = 0; i < scored.length; i++) {
|
||||
const idx = (candidateIndex + i) % scored.length;
|
||||
const c = scored[idx]!;
|
||||
if (!c.recipe.mealTypes.includes(mealType)) continue;
|
||||
if (o.respectRepeat && (recipeUseCount.get(c.recipe.id) ?? 0) >= repeatLimit) continue;
|
||||
if (o.avoidSig && signatureOf(c.recipe) === o.avoidSig) continue;
|
||||
if (o.noSoupAtDinner && mealType === "dinner" && isSoup(c.recipe)) continue;
|
||||
return { c, idx };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const hit =
|
||||
pick({ avoidSig: lastSignature, noSoupAtDinner: true, respectRepeat: true }) ??
|
||||
pick({ avoidSig: lastSignature, noSoupAtDinner: false, respectRepeat: true }) ??
|
||||
pick({ avoidSig: null, noSoupAtDinner: false, respectRepeat: true }) ??
|
||||
pick({ avoidSig: lastSignature, noSoupAtDinner: false, respectRepeat: false }) ??
|
||||
pick({ avoidSig: null, noSoupAtDinner: false, respectRepeat: false });
|
||||
if (!hit) continue;
|
||||
const chosen = hit.c;
|
||||
candidateIndex = hit.idx + 1;
|
||||
lastSignature = signatureOf(chosen.recipe);
|
||||
recipeUseCount.set(chosen.recipe.id, (recipeUseCount.get(chosen.recipe.id) ?? 0) + 1);
|
||||
usedRecipeIds.add(chosen.recipe.id);
|
||||
const expiring = chosen.coverage.expiringUsed[0];
|
||||
|
||||
Reference in New Issue
Block a user