feat: one-click shopping list from meal plan, fix ingredient merge bug

- Meal-plan page now has a direct "Add to shopping list" action for the
  currently-viewed week, instead of requiring a trip to Shopping Lists and
  manually typing the week's Monday date.
- Fixed a real under-shopping bug in shopping-lists/route.ts: when
  generating from a meal-plan week, ingredients appearing in more than one
  recipe were merged by keeping only the FIRST occurrence's quantity and
  silently dropping the rest. New mergeIngredients() (pantry-shopping-match.ts)
  groups by ingredientId (or normalized name+unit) and sums quantities
  across every recipe in the week; incompatible/unparseable units are kept
  as separate line items rather than guessed at.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-10 09:45:51 +02:00
parent 9c545a5bb3
commit d62e2a6383
6 changed files with 104 additions and 28 deletions
+5 -15
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db, shoppingLists, shoppingListItems, mealPlans, mealPlanEntries, recipeIngredients, pantryItems, eq, and, desc, inArray } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { applyPantryToItems } from "@/lib/pantry-shopping-match";
import { applyPantryToItems, mergeIngredients } from "@/lib/pantry-shopping-match";
const CreateSchema = z.object({
name: z.string().min(1).max(100),
@@ -54,20 +54,10 @@ export async function POST(req: NextRequest) {
where: inArray(recipeIngredients.recipeId, recipeIds),
});
// Simple merge by rawName (case-insensitive)
const merged = new Map<string, { rawName: string; quantity?: string; unit?: string; ingredientId: string | null }>();
for (const ing of ings) {
const key = ing.rawName.toLowerCase();
if (!merged.has(key)) {
merged.set(key, {
rawName: ing.rawName,
quantity: ing.quantity ?? undefined,
unit: ing.unit ?? undefined,
ingredientId: ing.ingredientId,
});
}
}
const mergedItems = Array.from(merged.values());
// Merge same ingredient across all of the week's recipes, summing quantities
// (grouped by ingredientId when available, else normalized name+unit) instead
// of listing it once per recipe or dropping every occurrence but the first.
const mergedItems = mergeIngredients(ings);
// Reduce/flag quantities already covered by the user's pantry. Conservative: never silently
// drops an item — fully-covered items are still inserted, flagged `inPantry`, so nothing