From b94b24640b4ee8f7c0b7c2f58edd7a1e84b603cf Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:30:07 -0500 Subject: [PATCH] feat: Adjust linked recipe unit and seperate when adding to shopping list (#7260) --- .../Recipe/RecipeDialogAddToShoppingList.vue | 149 ++++++++++-------- .../Domain/Recipe/RecipeIngredientEditor.vue | 7 +- frontend/lang/messages/en-US.json | 1 + 3 files changed, 91 insertions(+), 66 deletions(-) diff --git a/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue b/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue index c3bdf3956..649e39d23 100644 --- a/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue +++ b/frontend/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue @@ -86,6 +86,19 @@ class="text-center" > {{ recipeSection.recipeName }} + + + {{ $t("shopping-list.ingredient-of-recipe", { recipe: recipeSection.parentRecipe.name }) }} + preferences.value.viewAllLists], () => { } }); +function buildIngredientSections(ingredients: ShoppingListIngredient[]): ShoppingListIngredientSection[] { + let currentTitle = ""; + const onHandIngs: ShoppingListIngredient[] = []; + const sections = ingredients.reduce((acc, ing) => { + if (ing.ingredient.title) { + currentTitle = ing.ingredient.title; + } + + if (!acc.length || currentTitle !== acc[acc.length - 1].sectionName) { + if (acc.length) { + acc[acc.length - 1].ingredients.push(...onHandIngs); + onHandIngs.length = 0; + } + acc.push({ sectionName: currentTitle, ingredients: [] }); + } + + const householdsWithFood = ing.ingredient?.food?.householdsWithIngredientFood || []; + if (householdsWithFood.includes(currentHouseholdSlug.value)) { + onHandIngs.push(ing); + return acc; + } + + acc[acc.length - 1].ingredients.push(ing); + return acc; + }, [] as ShoppingListIngredientSection[]); + + if (sections.length) { + sections[sections.length - 1].ingredients.push(...onHandIngs); + } + return sections; +} + async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) { const recipeSectionMap = new Map(); + + function addSubRecipeToMap(ing: RecipeIngredient, parentQuantity: number, parentScale: number, parentRecipe: Recipe) { + const ref = ing.referencedRecipe!; + const key = ref.id || ref.slug || ""; + const ownIngs: ShoppingListIngredient[] = []; + const subRefIngs: RecipeIngredient[] = []; + + for (const subIng of ref.recipeIngredient ?? []) { + if (subIng.referencedRecipe) { + subRefIngs.push(subIng); + } + else { + const householdsWithFood = subIng.food?.householdsWithIngredientFood || []; + ownIngs.push({ + checked: !householdsWithFood.includes(currentHouseholdSlug.value), + ingredient: { ...subIng, quantity: (ing.quantity || 1) * (subIng.quantity || 1) }, + }); + } + } + + recipeSectionMap.set(key, { + recipeId: ref.id || "", + recipeName: ref.name || "", + recipeScale: parentQuantity * parentScale, + ingredientSections: buildIngredientSections(ownIngs), + parentRecipe, + }); + + subRefIngs.forEach(subIng => addSubRecipeToMap(subIng, (ing.quantity || 1) * (subIng.quantity || 1), parentScale, ref)); + } + for (const recipe of recipes) { if (!recipe.slug) { continue; @@ -291,81 +368,29 @@ async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) { continue; } - const shoppingListIngredients: ShoppingListIngredient[] = []; - function flattenRecipeIngredients(ing: RecipeIngredient, parentTitle = ""): ShoppingListIngredient[] { + const ownIngs: ShoppingListIngredient[] = []; + const subRefIngs: RecipeIngredient[] = []; + recipeData.recipeIngredient.forEach((ing) => { if (ing.referencedRecipe) { - // Recursively flatten all ingredients in the referenced recipe - return (ing.referencedRecipe.recipeIngredient ?? []).flatMap((subIng) => { - const calculatedQty = (ing.quantity || 1) * (subIng.quantity || 1); - // Pass the referenced recipe name as the section title - return flattenRecipeIngredients( - { ...subIng, quantity: calculatedQty }, - "", - ); - }); + subRefIngs.push(ing); } else { - // Regular ingredient const householdsWithFood = ing.food?.householdsWithIngredientFood || []; - return [{ + ownIngs.push({ checked: !householdsWithFood.includes(currentHouseholdSlug.value), - ingredient: { - ...ing, - title: ing.title || parentTitle, - }, - }]; - } - } - - recipeData.recipeIngredient.forEach((ing) => { - const flattened = flattenRecipeIngredients(ing, ""); - shoppingListIngredients.push(...flattened); - }); - - let currentTitle = ""; - const onHandIngs: ShoppingListIngredient[] = []; - const shoppingListIngredientSections = shoppingListIngredients.reduce((sections, ing) => { - if (ing.ingredient.title) { - currentTitle = ing.ingredient.title; - } - else if (ing.ingredient.referencedRecipe?.name) { - currentTitle = ing.ingredient.referencedRecipe.name; - } - - // If this is the first item in the section, create a new section - if (sections.length === 0 || currentTitle !== sections[sections.length - 1].sectionName) { - if (sections.length) { - // Add the on-hand ingredients to the previous section - sections[sections.length - 1].ingredients.push(...onHandIngs); - onHandIngs.length = 0; - } - sections.push({ - sectionName: currentTitle, - ingredients: [], + ingredient: ing, }); } - - // Store the on-hand ingredients for later - const householdsWithFood = (ing.ingredient?.food?.householdsWithIngredientFood || []); - if (householdsWithFood.includes(currentHouseholdSlug.value)) { - onHandIngs.push(ing); - return sections; - } - - // Add the ingredient to previous section - sections[sections.length - 1].ingredients.push(ing); - return sections; - }, [] as ShoppingListIngredientSection[]); - - // Add remaining on-hand ingredients to the previous section - shoppingListIngredientSections[shoppingListIngredientSections.length - 1].ingredients.push(...onHandIngs); + }); recipeSectionMap.set(recipe.slug, { recipeId: recipeData.id, recipeName: recipeData.name, recipeScale: recipeData.scale, - ingredientSections: shoppingListIngredientSections, + ingredientSections: buildIngredientSections(ownIngs), }); + + subRefIngs.forEach(ing => addSubRecipeToMap(ing, ing.quantity || 1, recipeData.scale, recipeData)); } recipeIngredientSections.value = Array.from(recipeSectionMap.values()); diff --git a/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue b/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue index 2707f7c63..22a0617fc 100644 --- a/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue +++ b/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue @@ -44,9 +44,8 @@ @@ -162,7 +161,7 @@ diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 41ca7705f..330f0ed3d 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -904,6 +904,7 @@ "all-lists": "All Lists", "create-shopping-list": "Create Shopping List", "from-recipe": "From Recipe", + "ingredient-of-recipe": "Ingredient of {recipe}", "list-name": "List Name", "new-list": "New List", "quantity": "Quantity: {0}",