munch-ease-frontend/src/components/shopping/shopping.js
jableader 531e4f057f Squashed commit of the following:
commit c5461420e7f1022019b79517d853a26301add9e9
Author: jableader <jacobdunk@gmail.com>
Date:   Sat Sep 28 14:55:18 2024 +1000

    meal scaling improvements

commit 37a444ff1fb5cbd1942161a8097a44fa7483b14c
Author: jableader <jacobdunk@gmail.com>
Date:   Sat Sep 28 13:13:06 2024 +1000

    Implementation
2024-09-28 14:57:32 +10:00

164 lines
No EOL
5.1 KiB
JavaScript

import units from '@/units.js';
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;
}
export function removeMealFromShoppingListSources(sources, meal) {
return sources.filter(source => source.meal?.id !== meal.id);
}
export function groupingToIngredients(grouping) {
const product = grouping.product;
const totals = [];
for (const total of grouping.totals) {
totals.push({ id: -1, line: `${total.quantity} ${total.unit} of ${product.name}`, preparation: '', name: product.name, product, ...total });
}
return totals;
}
function calculateTotals(sources) {
let byBaseUnit = {};
for (const { quantity, unit } of sources) {
const conversion = units.getConversionFactor(unit) ?? { unit, factor: 1 };
if (!byBaseUnit[conversion.unit]) {
byBaseUnit[conversion.unit] = {};
}
if (!byBaseUnit[conversion.unit][unit]) {
byBaseUnit[conversion.unit][unit] = { quantity, factor: conversion.factor };
}
else {
byBaseUnit[conversion.unit][unit].quantity += quantity;
}
}
const totals = {};
for (const baseUnit in byBaseUnit) {
if (Object.keys(byBaseUnit[baseUnit]).length === 1) {
const unit = Object.keys(byBaseUnit[baseUnit])[0];
const { quantity, } = byBaseUnit[baseUnit][unit];
totals[unit] = quantity;
}
else {
let total = 0;
for (const unit in byBaseUnit[baseUnit]) {
const { quantity, factor } = byBaseUnit[baseUnit][unit];
total += quantity / factor;
}
totals[baseUnit] = total;
}
}
return totals;
}
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 convertToBaseUnits(total) {
const baseUnits = {};
for (const unit in total) {
const conversion = units.getConversionFactor(unit) ?? { unit, factor: 1 };
baseUnits[conversion.unit] = (baseUnits[conversion.unit] ?? 0) + total[unit] / conversion.factor;
}
return baseUnits;
}
function getRemainingIngredients(requiredTotals, foundTotals) {
requiredTotals = convertToBaseUnits(requiredTotals);
foundTotals = convertToBaseUnits(foundTotals);
const remaining = {};
for (const unit in requiredTotals) {
const required = requiredTotals[unit];
const found = foundTotals[unit] ?? 0;
const remainingQuantity = required - found;
if (remainingQuantity > 0) {
remaining[unit] = remainingQuantity;
}
}
return remaining;
}
export function toProductsModel(shoppingList) {
const flattenedIngredients = requestsToSources(shoppingList.requests);
const foundIngredientsByProduct = groupBy(shoppingList.results.filter(f => f.found_date), found => found.product_id);
const requiredIngredientsByProduct = groupBy(flattenedIngredients, source => source.ingredient?.product?.id ?? "no-product");
const results = [];
for (const product_id in requiredIngredientsByProduct) {
const sources = requiredIngredientsByProduct[product_id];
const requiredIngredients = sources.map(source => source.ingredient);
const requiredTotals = calculateTotals(requiredIngredients);
const foundTotals = calculateTotals(foundIngredientsByProduct[product_id] ?? []);
let fullyFound = null, foundDate = null;
if (Object.keys(foundTotals).length === 0) {
foundDate = null;
fullyFound = false;
}
else {
foundDate = foundIngredientsByProduct[product_id][0].found_date;
fullyFound = Object.keys(getRemainingIngredients(requiredTotals, foundTotals)).length === 0;
}
results.push({
product: requiredIngredients[0].product,
totals: Object.entries(requiredTotals).map(([unit, quantity]) => ({ unit, quantity, })),
fullyFound, foundDate, sources,
});
}
return results;
}