diff --git a/src/components/shopping/CurrentShoppingListPage.vue b/src/components/shopping/CurrentShoppingListPage.vue
index b55c278..de783a8 100644
--- a/src/components/shopping/CurrentShoppingListPage.vue
+++ b/src/components/shopping/CurrentShoppingListPage.vue
@@ -5,19 +5,19 @@
No items to purchase
+
Need: - + , and {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }} @@ -31,9 +34,9 @@
-+
Already found or purchased: - + , and {{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }} @@ -113,23 +116,37 @@ import { calculateTotals } from '@/units.js'; export default { name: 'ShoppingListItem', - props: ['productGrouping' ], + props: ['shoppingListItemGroup' ], // { product: { ... }, OR name: 'string', shoppingListItems: { person, ingredient, list_id?, meal? }} where list_id is null if not yet purchased data() { return { expanded: false, } }, computed: { - remainingRequired() { - return calculateTotals(this.productGrouping.requested.map(r => r.ingredient)); + remainingRequiredTotals() { + return calculateTotals(this.shoppingListItemGroup.shoppingListItems.map(item => item.ingredient)); }, - expectedExisting() { - return calculateTotals(this.productGrouping.purchased.map(r => r.ingredient)); + expectedExistingTotals() { + const purchasedNotEaten = this.shoppingListItemGroup.shoppingListItems.filter(item => item.list_id && !(item?.meal?.consumed_date)); + + return calculateTotals(purchasedNotEaten.map(item => item.ingredient)); + }, + required() { + return this.shoppingListItemGroup.shoppingListItems.filter(item => !item.list_id); + }, + purchased() { + return this.shoppingListItemGroup.shoppingListItems.filter(item => item.list_id); }, lastPurchased() { - return this.productGrouping.purchased.reduce((latest, source) => { - return (source.shop.created_date && latest > source.shop.created_date) ? latest : source.shop.created_date; - }, new Date(0)); + const purchasedItems = this.shoppingListItemGroup.shoppingListItems.filter(item => item.list_id); + if (purchasedItems.length === 0) { + return null; + } + // Find the most recently purchased item + return purchasedItems.reduce((latest, item) => { + const itemDate = item?.meal?.suggested_date || item?.created_at; + return (!latest || (itemDate && itemDate > latest)) ? itemDate : latest; + }, null); } }, methods: { diff --git a/src/components/shopping/shopping.js b/src/components/shopping/shopping.js index 5099fef..408338e 100644 --- a/src/components/shopping/shopping.js +++ b/src/components/shopping/shopping.js @@ -1,158 +1,35 @@ -function scaleFactor(mealRecipe) { - const numRecipe = mealRecipe.recipe.serves; - const numRequested = mealRecipe.servings; - - return numRequested / numRecipe; +export function groupsToItems(groups) { + return groups.map(group => group.shoppingListItems).flat(); } -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; -} - -var emptyProductId = -1; -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)); - } - } - - for (const source of sources) { - if (!source.ingredient.product) { - source.ingredient.product = { id: emptyProductId--, name: source.ingredient.name, }; - } - } - - 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({ shop, ...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: [], }; +export function itemsToGroups(shoppingListItems) { + const ingredients_by_product_id = {}; + const ingredients_by_name = {}; + for (const item of shoppingListItems) { + if (item.ingredient.product) { + let group = ingredients_by_product_id[item.ingredient.product.id]; + if (!group) { + group = ingredients_by_product_id[item.ingredient.product.id] = { + product: item.ingredient.product, + shoppingListItems: [] + }; } - - grouped[source.ingredient.product.id].requested.push(source); + group.shoppingListItems.push(item); } - - 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: [], }; + else { + let group = ingredients_by_name[item.ingredient.name]; + if (!group) { + group = ingredients_by_name[item.ingredient.name] = { + name: item.ingredient.name, + shoppingListItems: [] + }; } - - grouped[source.ingredient.product.id].purchased.push(source); + group.shoppingListItems.push(item); } } - // Only return values, array - return grouped; -} - -export function purchasedToProductModel(shoppingList) { - const purchasedProductIds = new Set(shoppingList.results.map(r => r.product?.id ?? 0)); - 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; + return [ + ...Object.values(ingredients_by_product_id), + ...Object.values(ingredients_by_name) + ] } \ No newline at end of file diff --git a/src/data.js b/src/data.js index a44f980..c3d7a70 100644 --- a/src/data.js +++ b/src/data.js @@ -2,9 +2,9 @@ const BASE_URL = window.location.href.replace(/^(https?:\/\/[^/]+).*/, "$1/api") const datesToFix = { Meal: { fields: [ "suggested_date", "purchase_date", "consumed_date" ] }, - CurrentShoppingList: { dependants: l => ({ ShoppingListRequest: l.requests, ShoppingList: l.overlapping_previous_shops }) }, + CurrentShoppingList: { dependants: l => ({ ShoppingListItem: [l.outstanding_items, l.requested_meals, l.purchased_items], Meal: Object.values(l.meals_lookup), Recipe: Object.values(l.recipes_lookup), Ingredient: Object.values(l.ingredients_lookup), ShoppingList: Object.values(l.shopping_list_lookup) }) }, ShoppingList: { fields: [ "created_date" ], dependants: l => ({ ShoppingListItem: l.items }) }, - ShoppingListItem: { fields: [ "created_date" ], dependants: r => ({ Meal: r.meal }) }, + ShoppingListItem: { fields: [ "created_date" ], dependants: r => ({ Meal: r.meal, Recipe: r.recipe, Ingredient: r.ingredient, ShoppingList: r.list }) }, Recipe: { fields: [ "date_created", "date_hidden" ], }, }; @@ -42,6 +42,26 @@ const fixDates = (obj, type) => { } } +const setShoppingListReferences = (currentShoppingList) => { + if (!currentShoppingList) return; + + var allRequests = [...currentShoppingList.outstanding_items, ...currentShoppingList.requested_meals, ...currentShoppingList.purchased_items]; + + for (const item of allRequests) { + if (item.ingredient_id) { + item.ingredient = currentShoppingList.ingredients_lookup[item.ingredient_id]; + } + if (item.meal_id) { + item.meal = currentShoppingList.meals_lookup[item.meal_id]; + } + if (item.list_id) { + item.list = currentShoppingList.shopping_list_lookup[item.list_id]; + } + if (item.recipe_id) { + item.recipe = currentShoppingList.recipes_lookup[item.recipe_id]; + } + } +} let user = null; export default { @@ -212,8 +232,10 @@ export default { }, async getMyShoppingList() { const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", { credentials: "include" }); - - return await response.json(); + const ingredients = await response.json(); + fixDates(ingredients, 'Ingredient'); + + return ingredients; }, async saveMyShoppingList(list) { const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", { @@ -225,7 +247,10 @@ export default { body: JSON.stringify(list), }); - return await response.json(); + const ingredients = await response.json(); + fixDates(ingredients, 'Ingredient'); + + return ingredients; }, async getShoppingList(id) { const response = await fetch(BASE_URL + `/shopping/${encodeURIComponent(id)}`); @@ -238,7 +263,8 @@ export default { const response = await fetch(BASE_URL + "/shopping/current"); const lst = await response.json(); fixDates(lst, "CurrentShoppingList"); - + setShoppingListReferences(lst); + return lst; }, async purchaseItems(completed_requests) {