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:
@@ -7,6 +7,7 @@ import { db, mealPlans, mealPlanMembers, recipes, userNutritionGoals, eq, and, d
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { MealPlanner } from "@/components/meal-plan/meal-planner";
|
||||
import { ShareMealPlanButton } from "@/components/meal-plan/share-meal-plan-button";
|
||||
import { NewShoppingListButton } from "@/components/meal-plan/new-shopping-list-button";
|
||||
import { WeeklyNutritionBar } from "@/components/nutrition/weekly-nutrition-bar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ExportMarkdownButton } from "@/components/shared/export-markdown-button";
|
||||
@@ -100,6 +101,10 @@ export default async function MealPlanPage({
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<ShareMealPlanButton weekStart={weekStart} />
|
||||
<NewShoppingListButton
|
||||
defaultWeekStart={weekStart}
|
||||
defaultName={formatMessage(msgs.mealPlan.shoppingListWeekName, { week: label })}
|
||||
/>
|
||||
<Link href="/shopping-lists" className={cn(buttonVariants({ variant: "outline", size: "sm" }))}>
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
{msgs.mealPlan.shoppingLists}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user