function scaleFactor(mealRecipe) { const numRecipe = mealRecipe.recipe.serves; const numRequested = mealRecipe.servings; return numRequested / numRecipe; } export function mealToShoppingListSources(meal) { const sources = []; for (const mealRecipe of meal.recipes) { const scale = scaleFactor(mealRecipe); for (const ingredient of mealRecipe.recipe.ingredients) { const quantity = ingredient.quantity * scale; sources.push({ meal, mealRecipe, recipe: mealRecipe.recipe, ingredient: { ...ingredient, quantity, }, }); } } for (const ingredient of meal.extra_ingredients) { sources.push({ ingredient, meal, }); } return sources; } export function requestsToSources(requests) { const sources = []; for (const request of requests) { if (request.ingredient) { sources.push(request); } else if (request.meal) { sources.push(...mealToShoppingListSources(request.meal)); } } return sources; } function groupBy(groups, keyFn) { const grouped = {}; for (const group of groups) { const key = keyFn(group); if (!grouped[key]) { grouped[key] = []; } grouped[key].push(group); } return grouped; } function resultsToSources(shop) { return shop.results.map(result => ({ shop, ingredient: { product: result.product, unit: result.unit, quantity: result.quantity, }, })); } function getOutstandingGroupedByMeal(currentShoppingList) { // { meal_id: { purchased: [ sources, ], required: [ sources, ], }, } const requestedMeals = groupBy(requestsToSources(currentShoppingList.requests), source => source.meal?.id); for (const meal_id in requestedMeals) { requestedMeals[meal_id] = { id: parseInt(meal_id), purchased: [], required: requestedMeals[meal_id], }; } for (const shop of currentShoppingList.overlapping_previous_shops) { const shopResults = groupBy(resultsToSources(shop), source => source.ingredient.product.id); const purchasedSources = requestsToSources(shop.requests) .filter(source => source.meal && requestedMeals[source.meal.id]) .filter(source => source.ingredient.product.id in shopResults); for (const source of purchasedSources) { requestedMeals[source.meal.id].purchased.push(source); requestedMeals[source.meal.id].required = requestedMeals[source.meal.id].required.filter(required => required.ingredient.product.id !== source.ingredient.product.id); } } return requestedMeals; } export function getCompletedRequests(currentShoppingList, results) { const newShoppingList = { requests: currentShoppingList.requests, results }; const fakeCurrentShoppingList = { requests: currentShoppingList.requests, overlapping_previous_shops: [ newShoppingList, ...currentShoppingList.overlapping_previous_shops ], }; const requestedMealResults = Object.values(getOutstandingGroupedByMeal(fakeCurrentShoppingList)); const completedMeals = new Set(requestedMealResults.filter(mealGroup => mealGroup.required.length === 0).map(mealGroup => mealGroup.id)); const purchasedProducts = new Set(results.map(result => result.product.id)); const completedSources = currentShoppingList.requests.filter(request => { if (request.meal) { return completedMeals.has(request.meal.id); } return purchasedProducts.has(request.ingredient.product.id); }); return sourcesToRequests(completedSources); } export function sourcesToRequests(sources) { const uniqueMeals = sources.filter(source => source.meal) .map(source => source.meal) .reduce((acc, meal) => { acc[meal.id] = { meal, meal_id: meal.id, }; return acc; }, {}); const directIngredients = sources.filter(source => !source.meal); return [ ...Object.values(uniqueMeals), ...directIngredients, ]; } export function toProductsModel(currentShoppingList) { const requestedMealResults = getOutstandingGroupedByMeal(currentShoppingList); // Transform into { product_id: { product, requested: [ sources, ], purchased: [ sources, ], }, } const grouped = {}; for (const meal_id in requestedMealResults) { for (const source of requestedMealResults[meal_id].required) { if (!grouped[source.ingredient.product.id]) { grouped[source.ingredient.product.id] = { product: source.ingredient.product, requested: [], purchased: [], }; } grouped[source.ingredient.product.id].requested.push(source); } for (const source of requestedMealResults[meal_id].purchased) { if (!grouped[source.ingredient.product.id]) { grouped[source.ingredient.product.id] = { product: source.ingredient.product, requested: [], purchased: [], }; } grouped[source.ingredient.product.id].purchased.push(source); } } // Only return values, array return grouped; } export function purchasedToProductModel(shoppingList) { const purchasedProductIds = new Set(shoppingList.results.map(r => r.product.id)); const purchasedSources = requestsToSources(shoppingList.requests).filter(r => purchasedProductIds.has(r.ingredient.product.id)); const grouped = {}; for (const source of purchasedSources) { const product = source.ingredient.product; if (!grouped[product.id]) { grouped[product.id] = { product, requested: [], purchased: [], }; } grouped[product.id].requested.push(source); } return grouped; }