Compare commits

..

6 commits

Author SHA1 Message Date
7705c3113c Improved group matching 2025-07-29 09:07:44 +10:00
6cc9f8fef7 Fixed Purchased Meals 2025-07-28 22:35:47 +10:00
e72c84b18e Current Shopping Page fixes 2025-07-28 21:50:29 +10:00
ce3e255eb4 FIrst render 2025-07-28 20:51:17 +10:00
67cbb370ed Can edit own requests 2025-07-27 15:25:37 +10:00
728532a7c8 first working 2025-07-27 11:58:57 +10:00
8 changed files with 220 additions and 245 deletions

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<title>ic_fluent_missing_metadata_24_regular</title>
<desc>Created with Sketch.</desc>
<g id="🔍-System-Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ic_fluent_missing_metadata_24_regular" fill="#212121" fill-rule="nonzero">
<path d="M19.7501,2 C20.9927,2 22.0001,3.00736 22.0001,4.25 L22.0001,9.71196 C22.0001,10.5738 21.6578,11.4003 21.0484,12.0098 L21.0222,12.0361 C20.5797,11.7503 20.1003,11.5167 19.5928,11.3442 L19.9876,10.9492 C20.3157,10.6211 20.5001,10.176 20.5001,9.71196 L20.5001,4.25 C20.5001,3.83579 20.1643,3.5 19.7501,3.5 L14.2847,3.5 C13.8202,3.5 13.3748,3.68467 13.0465,4.01333 L4.53436,12.5358 C3.86414,13.2207 3.86923,14.3191 4.54908,14.9977 L9.0103,19.4522 C9.64819,20.0877 10.6535,20.1309 11.3408,19.5826 C11.5052,20.0689 11.7256,20.5295 11.9943,20.9567 C10.7373,21.7569 9.05064,21.6098 7.95104,20.5143 L3.48934,16.0592 C2.21887,14.7913 2.21724,12.7334 3.48556,11.4632 L11.9852,2.95334 C12.5948,2.34297 13.4221,2 14.2847,2 L19.7501,2 Z M17,5.50218 C17.8284,5.50218 18.5,6.17374 18.5,7.00216 C18.5,7.83057 17.8284,8.50213 17,8.50213 C16.1716,8.50213 15.5001,7.83057 15.5001,7.00216 C15.5001,6.17374 16.1716,5.50218 17,5.50218 Z M23,17.5 C23,14.4624 20.5376,12 17.5,12 C14.4624,12 12,14.4624 12,17.5 C12,20.5376 14.4624,23 17.5,23 C20.5376,23 23,20.5376 23,17.5 Z M16.8755,20.5045 C16.8755,20.1596 17.1551,19.88 17.5,19.88 C17.8449,19.88 18.1245,20.1596 18.1245,20.5045 C18.1245,20.8494 17.8449,21.129 17.5,21.129 C17.1551,21.129 16.8755,20.8494 16.8755,20.5045 Z M15.6467,15.9574 C15.6357,14.8205 16.4521,14.0031 17.5,14.0031 C18.5311,14.0031 19.3534,14.8489 19.3534,15.9526 C19.3534,16.5186 19.1682,16.866 18.6905,17.4003 L18.4247,17.6908 L18.3238,17.8063 C18.0765,18.0981 18,18.2684 18,18.5006 C18,18.7767 17.7762,19.0006 17.5,19.0006 C17.2239,19.0006 17,18.7767 17,18.5006 C17,17.9255 17.1868,17.5749 17.6711,17.0333 L17.9365,16.7432 L18.0355,16.63 C18.2782,16.3437 18.3534,16.1769 18.3534,15.9526 C18.3534,15.395 17.9724,15.0031 17.5,15.0031 C17.0063,15.0031 16.6411,15.3688 16.6465901,15.9478 C16.6493,16.2239 16.4276,16.4499 16.1514,16.4526 C15.8753,16.4552 15.6493,16.2335 15.6467,15.9574 Z" id="🎨-Color">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,7 +1,7 @@
<template>
<div class="compact-parse-results">
<p class="parse-element teaser-image" v-if="ingredient.product && ingredient.product.img_small">
<img :src="ingredient.product.img_small" />
<p class="parse-element teaser-image">
<img :src="ingredient.product?.img_small ?? require('@/assets/missing-product.svg')" />
</p>
<p class="ingredient-details">
<span class="parse-element quantity" :class="{ missing: !(ingredient?.quantity)}">{{ ingredient?.quantity || 'qty' }}</span>

View file

@ -5,19 +5,19 @@
<meal-selection-list @meal-selected="mealSelected" @meal-unselected="mealUnselected" :checked="includedMeals" :meals="availableMeals" />
<ul class="full-shopping-list">
<li v-for="item in productGroupsToPurchase" :key="item.id" class="selectable" :class="{ 'selected': isSelected(item) }" @click="toggleSelect(item)">
<shopping-list-item :productGrouping="item" />
<li v-for="group in outstandingItemGroups" :key="group.id" class="selectable" :class="{ 'selected': isSelected(group) }" @click="toggleSelect(group)">
<shopping-list-item :shopping-list-item-group="group" />
</li>
</ul>
<div v-if="productGroupsToPurchase.length === 0">
<div v-if="outstandingItemGroups.length === 0">
<p>
No items to purchase
</p>
</div>
<div class="purchased-slider">
<span v-if="purchasedGroups.length === 0"></span>
<span v-if="purchasedItemGroups.length === 0"></span>
<button v-else-if="showPurchased" @click="showPurchased=false" > Hide Purchased </button>
<button v-else @click="showPurchased=true"> Show Purchased </button>
@ -29,8 +29,8 @@
Purchased Items
</h4>
<ul class="full-shopping-list">
<li v-for="item in purchasedGroups" :key="item.id">
<shopping-list-item :productGrouping="item" />
<li v-for="item in purchasedItemGroups" :key="item.id">
<shopping-list-item :shopping-list-item-group="item" />
</li>
</ul>
</div>
@ -43,7 +43,7 @@
<!-- Display 'Stocked', 'Purchased' and 'Cancel' buttons in a vertical stack fixed to the bottom of the screen when any elements are selected -->
<div class="footer-buttons" v-if="selected.length">
<p v-if="selected.length === 1">
Mark '{{ selected[0].product.name }}' as
Mark '{{ selected[0].product?.name ?? selected[0].name }}' as
</p>
<p v-else>
Mark {{ selected.length }} items as
@ -135,26 +135,28 @@ button img {
<script>
import alert from '@/alert.js'
import data from '@/data.js'
import { toProductsModel, getCompletedRequests, sourcesToRequests } from './shopping.js'
import { itemsToGroups, groupsToItems } from './shopping.js'
import MealSelectionList from './MealSelectionList.vue'
import ShoppingListItem from './ShoppingListItem.vue'
async function saveShoppingList(storeName, currentShoppingList, productGroupsPurchased) {
const results = productGroupsPurchased.map(item => item.requested.map(r => ({
product_id: r.ingredient.product_id,
product: r.ingredient.product,
quantity: r.ingredient.quantity,
unit: r.ingredient.unit,
list_id: -1,
}))).flat();
async function saveShoppingList(outstandingItemGroups) {
const items = groupsToItems(outstandingItemGroups);
if (items.length === 0) {
alert.show({ type: 'error', message: 'No items selected.' });
return;
}
const requestedSources = productGroupsPurchased.map(item => item.requested).flat();
const completedRequests = getCompletedRequests(currentShoppingList, results);
const servicedRequests = sourcesToRequests(requestedSources);
return await data.purchaseShoppingList(items);
}
return await data.purchaseItems(storeName, servicedRequests, results, completedRequests);
const groupsMatch = (a, b) => {
if (!!a.product != !!b.product) return false;
if (a.name) return a.name === b.name;
return a.product.id === b.product.id;
}
export default {
@ -173,69 +175,74 @@ export default {
const to = new Date();
to.setDate(to.getDate() + 7);
return { from, to, availableMeals: [], purchasedMeals: [], shoppingList: null, includedMeals: [], productGroupsToPurchase: [], purchasedGroups: [], selected: [], showPurchased: false }
return { from, to, shoppingList: null, selected: [], showPurchased: false }
},
async beforeMount() {
this.loadData();
},
watch: {
shoppingList: {
handler: 'updateLists',
deep: true
computed: {
outstandingItemGroups() {
return itemsToGroups(this.shoppingList?.outstanding_items ?? []);
},
purchasedItemGroups() {
return itemsToGroups(this.shoppingList?.purchased_items ?? []);
},
purchasedMeals() {
return Object.values(this.shoppingList?.meals_lookup ?? {}).filter(m => m.purchase_date).sort((a, b) => a.suggested_date - b.suggested_date);
},
availableMeals() {
const meals = { ...this.shoppingList?.meals_lookup ?? {} };
this.upcomingMeals?.forEach(m => {
if (!meals[m.id]) {
meals[m.id] = m;
}
});
return Object.values(meals).filter(m => !m.purchase_date).sort((a, b) => a.suggested_date - b.suggested_date);
},
includedMeals() {
return this.shoppingList?.requested_meals.map(m => m.meal) ?? [];
}
},
methods: {
async loadData() {
this.upcomingMeals = await data.getUpcomingMeals(this.from, this.to);
this.shoppingList = await data.getCurrentShoppingList();
const upcomingMeals = await data.getUpcomingMeals(this.from, this.to);
const requestedMeals = this.shoppingList.requests.map(r => r.meal).filter(m => m);
const meals = {};
requestedMeals.forEach(m => meals[m.id] = m);
upcomingMeals.forEach(m => meals[m.id] = m);
this.purchasedMeals = Object.values(meals).filter(m => m.purchase_date);
this.availableMeals = Object.values(meals).filter(m => !m.purchase_date);
},
async updateLists() {
if (!this.shoppingList)
return;
this.includedMeals = this.shoppingList.requests.map(r => r.meal).filter(m => m);
const productGroups = Object.values(toProductsModel(this.shoppingList));
this.productGroupsToPurchase = productGroups.filter(m => m.requested.length > 0);
this.purchasedGroups = productGroups.filter(m => m.requested.length === 0);
},
async mealSelected(meal) {
const request = await data.requestMeal(meal.id);
this.shoppingList.requests.push(request);
await data.requestMeal(meal.id);
await this.loadData();
},
async mealUnselected(meal) {
await data.unrequestMeal(meal.id);
this.shoppingList.requests = this.shoppingList.requests.filter(r => r.meal?.id !== meal.id);
await this.loadData();
},
async markFound() {
await saveShoppingList('', this.shoppingList, this.selected);
await saveShoppingList(this.selected);
this.selected = [];
this.loadData();
await this.loadData();
},
async markPurchased() {
const shoppingList = await saveShoppingList('', this.shoppingList, this.selected);
const shoppingList = await saveShoppingList(this.selected);
if (!shoppingList || !shoppingList.id) {
alert.show({ type: 'error', message: 'Failed to purchase.' });
return;
}
this.selected = [];
this.$router.push(`/shopping/${shoppingList.id}`);
},
toggleSelect(item) {
const index = this.selected.findIndex(i => i === item);
const index = this.selected.findIndex(i => groupsMatch(i, item));
if (index === -1)
this.selected.push(item);
else
this.selected.splice(index, 1);
},
isSelected(item) {
return this.selected.some(i => i === item);
return this.selected.some(i => groupsMatch(i, item));
}
}
}

View file

@ -56,11 +56,11 @@ export default {
},
methods: {
async updateShoppingList(save = false) {
const requests = save ?
const new_ingredients = save ?
await data.saveMyShoppingList(this.ingredients) :
await data.getMyShoppingList();
this.ingredients = requests.map(r => r.ingredient);
this.ingredients = new_ingredients;
},
addIngredient() {
this.ingredients = [{ id: -1 }, ...this.ingredients];

View file

@ -1,12 +1,14 @@
<template>
<h3>Purchased {{ shoppingList ? ago(shoppingList.created_date) : '' }}</h3>
<div v-if="includedMeals.length > 0">
<h4>Included Meals</h4>
<meal-selection-list :checked="includedMeals" :meals="includedMeals" :disabled="true" />
</div>
<ul class="full-shopping-list">
<li v-for="item in listByProduct" :key="item.id">
<shopping-list-item :productGrouping="item" />
<shopping-list-item :shopping-list-item-group="item" />
</li>
</ul>
</template>
@ -32,7 +34,7 @@
import { ago } from '@/dateformats.js'
import data from '@/data.js'
import { purchasedToProductModel } from './shopping.js'
import { itemsToGroups } from './shopping.js'
import MealSelectionList from './MealSelectionList.vue'
import ShoppingListItem from './ShoppingListItem.vue'
@ -43,17 +45,31 @@ export default {
props: {
id: [String, Number]
},
computed: {
includedMeals() {
if (!this.shoppingList) return [];
const seenMeals = new Set();
return this.shoppingList.items
.map(item => item.meal)
.filter(meal => {
if (!meal) return false;
if (seenMeals.has(meal.id)) return false;
seenMeals.add(meal.id);
return true;
});
},
listByProduct() {
return this.shoppingList ? itemsToGroups(this.shoppingList.items) : [];
}
},
data() {
return {
shoppingList: null,
includedMeals: [],
listByProduct: [],
}
},
async beforeMount() {
this.shoppingList = await data.getShoppingList(this.id);
this.includedMeals = this.shoppingList.requests.map(r => r.meal).filter(m => m);
this.listByProduct = purchasedToProductModel(this.shoppingList);
},
methods: {
ago

View file

@ -2,21 +2,24 @@
<!-- Show a card of the product with the total, taking up the available space and have an expanding section to view sources -->
<div class="shopping-list-item">
<img :src="productGrouping.product.img_small" class="product-image" />
<img :src="`${ shoppingListItemGroup.product?.img_small ?? require('@/assets/missing-product.svg') }`" class="product-image" />
<div class="product-details">
<h3 class="header">
<strong><a :href="productGrouping.product.link">{{ productGrouping.product.name }}</a></strong>,
<strong>
<a v-if="shoppingListItemGroup.product?.link" :href="shoppingListItemGroup.product?.link">{{ shoppingListItemGroup.product?.name }}</a>
<span v-else>{{ shoppingListItemGroup.name }}</span>
</strong>,
<small>
<span v-for="(total, index) in remainingRequired" :key="total.id">
<span v-for="(total, index) in remainingRequiredTotals" :key="total.id">
<span v-if="index">, </span>
<span>{{ formatQuantity(total.quantity) }}&nbsp;{{ total.unit }}</span>
</span>
<span class="found-marker partial" v-if="productGrouping.purchased.length > 0"> {{ getFriendlyDate(lastPurchased) }}</span>
<span class="found-marker partial" v-if="purchased.length > 0"> {{ getFriendlyDate(lastPurchased) }}</span>
</small>
</h3>
<p class="sources" v-if="productGrouping.requested.length > 0">
<p class="sources" v-if="required.length > 0">
<strong>Need: </strong>
<span v-for="(source, index) in productGrouping.requested" :key="source.id">
<span v-for="(source, index) in required" :key="source.id">
<span v-if="index">, and </span>
<span v-if="source.person">
{{ formatQuantity(source.ingredient.quantity) }}&nbsp;{{ source.ingredient.unit }} for {{ source.person.name }}
@ -31,9 +34,9 @@
</span>
</span>
</p>
<p v-if="productGrouping.purchased.length > 0">
<p v-if="purchased.length > 0">
<strong>Already found or purchased: </strong>
<span v-for="(source, index) in productGrouping.purchased" :key="source.id">
<span v-for="(source, index) in purchased" :key="source.id">
<span v-if="index">, and </span>
<span v-if="source.person">
{{ formatQuantity(source.ingredient.quantity) }}&nbsp;{{ 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: {

View file

@ -1,151 +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, }, });
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: []
};
}
group.shoppingListItems.push(item);
}
else {
let group = ingredients_by_name[item.ingredient.name];
if (!group) {
group = ingredients_by_name[item.ingredient.name] = {
name: item.ingredient.name,
shoppingListItems: []
};
}
group.shoppingListItems.push(item);
}
}
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({ 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: [], };
}
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;
return [
...Object.values(ingredients_by_product_id),
...Object.values(ingredients_by_name)
]
}

View file

@ -2,10 +2,10 @@ 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 }) },
ShoppingList: { fields: [ "created_date", "purchased_date" ], dependants: l => ({ ShoppingListResult: l.results, ShoppingListRequest: l.requests }) },
ShoppingListResult: { fields: [ "found_date", "created_date" ], },
ShoppingListRequest: { fields: [ "created_date" ], dependants: r => ({ Meal: r.meal }) },
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) }) },
PurchasedShoppingList: { dependants: l => ({ ShoppingListItem: l.items, Meal: Object.values(l.meals_lookup), Recipe: Object.values(l.recipes_lookup), Ingredient: Object.values(l.ingredients_lookup), ShoppingList: l.list }) },
ShoppingList: { fields: [ "created_date" ], dependants: l => ({ ShoppingListItem: l.items }) },
ShoppingListItem: { fields: [ "created_date" ], dependants: r => ({ Meal: r.meal, Recipe: r.recipe, Ingredient: r.ingredient, ShoppingList: r.list }) },
Recipe: { fields: [ "date_created", "date_hidden" ], },
};
@ -43,6 +43,39 @@ const fixDates = (obj, type) => {
}
}
const setPurchasedShoppingListReferences = (purchasedShoppingList) => {
if (!purchasedShoppingList) return;
const { ingredients_lookup, meals_lookup, recipes_lookup, } = purchasedShoppingList;
const shopping_list_lookup = { [purchasedShoppingList.list.id]: purchasedShoppingList.list };
setShoppingListItemReferences(purchasedShoppingList.list.items, ingredients_lookup, meals_lookup, recipes_lookup, shopping_list_lookup);
}
const setCurrentShoppingListReferences = (currentShoppingList) => {
if (!currentShoppingList) return;
const { ingredients_lookup, meals_lookup, recipes_lookup, shopping_list_lookup } = currentShoppingList;
const allShoppingListItems = [...currentShoppingList.outstanding_items, ...currentShoppingList.requested_meals, ...currentShoppingList.purchased_items];
setShoppingListItemReferences(allShoppingListItems, ingredients_lookup, meals_lookup, recipes_lookup, shopping_list_lookup);
}
const setShoppingListItemReferences = (shoppingListItems, ingredients_lookup, meals_lookup, recipes_lookup, shopping_list_lookup) => {
if (!shoppingListItems) return;
for (const item of shoppingListItems) {
if (item.ingredient_id) {
item.ingredient = ingredients_lookup[item.ingredient_id];
}
if (item.meal_id) {
item.meal = meals_lookup[item.meal_id];
}
if (item.list_id) {
item.list = shopping_list_lookup[item.list_id];
}
if (item.recipe_id) {
item.recipe = recipes_lookup[item.recipe_id];
}
}
}
let user = null;
export default {
@ -213,10 +246,10 @@ export default {
},
async getMyShoppingList() {
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", { credentials: "include" });
const requests = await response.json();
fixDates(requests, "ShoppingListRequest");
const ingredients = await response.json();
fixDates(ingredients, 'Ingredient');
return requests;
return ingredients;
},
async saveMyShoppingList(list) {
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", {
@ -228,39 +261,44 @@ export default {
body: JSON.stringify(list),
});
const requests = await response.json();
fixDates(requests, "ShoppingListRequest");
const ingredients = await response.json();
fixDates(ingredients, 'Ingredient');
return requests;
return ingredients;
},
async getShoppingList(id) {
const response = await fetch(BASE_URL + `/shopping/${encodeURIComponent(id)}`);
const lst = await response.json();
fixDates(lst, "ShoppingList");
return lst;
const purchasedShoppingList = await response.json();
fixDates(purchasedShoppingList, "PurchasedShoppingList");
setPurchasedShoppingListReferences(purchasedShoppingList);
return purchasedShoppingList.list;
},
async getCurrentShoppingList() {
const response = await fetch(BASE_URL + "/shopping/current");
const lst = await response.json();
fixDates(lst, "CurrentShoppingList");
setCurrentShoppingListReferences(lst);
return lst;
},
async purchaseItems(storeName, requests, results, completedRequests) {
async purchaseShoppingList(completed_requests) {
const response = await fetch(BASE_URL + "/shopping/", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ requests, results, store_name: storeName, completed_requests: completedRequests }),
body: JSON.stringify({ items: completed_requests }),
});
const lst = await response.json();
fixDates(lst, "ShoppingList");
const purchasedShoppingList = await response.json();
fixDates(purchasedShoppingList, "PurchasedShoppingList");
setPurchasedShoppingListReferences(purchasedShoppingList);
return lst;
return purchasedShoppingList.list;
},
async requestMeal(meal_id) {
const response = await fetch(BASE_URL + "/shopping/current/meals/me", {
@ -273,7 +311,7 @@ export default {
});
const requests = await response.json();
fixDates(requests, "ShoppingListRequest");
fixDates(requests, "ShoppingListItem");
return requests;
},
@ -283,8 +321,9 @@ export default {
credentials: "include",
});
const requests = await response.json();
fixDates(requests, "ShoppingListRequest");
if (!response.ok) {
throw new Error("Failed to unrequest meal");
}
},
async getPersonsInHome() {
const response = await fetch(BASE_URL + "/persons");