Compare commits
No commits in common. "7705c3113c1cb8baf24f4ff8113197a2f4dfde12" and "212e89ca493e797ee136f0a944698dedeb107118" have entirely different histories.
7705c3113c
...
212e89ca49
8 changed files with 248 additions and 223 deletions
|
|
@ -1,12 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="compact-parse-results">
|
||||
<p class="parse-element teaser-image">
|
||||
<img :src="ingredient.product?.img_small ?? require('@/assets/missing-product.svg')" />
|
||||
<p class="parse-element teaser-image" v-if="ingredient.product && ingredient.product.img_small">
|
||||
<img :src="ingredient.product.img_small" />
|
||||
</p>
|
||||
<p class="ingredient-details">
|
||||
<span class="parse-element quantity" :class="{ missing: !(ingredient?.quantity)}">{{ ingredient?.quantity || 'qty' }}</span>
|
||||
|
|
|
|||
|
|
@ -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="group in outstandingItemGroups" :key="group.id" class="selectable" :class="{ 'selected': isSelected(group) }" @click="toggleSelect(group)">
|
||||
<shopping-list-item :shopping-list-item-group="group" />
|
||||
<li v-for="item in productGroupsToPurchase" :key="item.id" class="selectable" :class="{ 'selected': isSelected(item) }" @click="toggleSelect(item)">
|
||||
<shopping-list-item :productGrouping="item" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="outstandingItemGroups.length === 0">
|
||||
<div v-if="productGroupsToPurchase.length === 0">
|
||||
<p>
|
||||
No items to purchase
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="purchased-slider">
|
||||
<span v-if="purchasedItemGroups.length === 0"></span>
|
||||
<span v-if="purchasedGroups.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 purchasedItemGroups" :key="item.id">
|
||||
<shopping-list-item :shopping-list-item-group="item" />
|
||||
<li v-for="item in purchasedGroups" :key="item.id">
|
||||
<shopping-list-item :productGrouping="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 ?? selected[0].name }}' as
|
||||
Mark '{{ selected[0].product.name }}' as
|
||||
</p>
|
||||
<p v-else>
|
||||
Mark {{ selected.length }} items as
|
||||
|
|
@ -135,28 +135,26 @@ button img {
|
|||
|
||||
<script>
|
||||
|
||||
import alert from '@/alert.js'
|
||||
|
||||
import data from '@/data.js'
|
||||
import { itemsToGroups, groupsToItems } from './shopping.js'
|
||||
import { toProductsModel, getCompletedRequests, sourcesToRequests } from './shopping.js'
|
||||
|
||||
import MealSelectionList from './MealSelectionList.vue'
|
||||
import ShoppingListItem from './ShoppingListItem.vue'
|
||||
|
||||
async function saveShoppingList(outstandingItemGroups) {
|
||||
const items = groupsToItems(outstandingItemGroups);
|
||||
if (items.length === 0) {
|
||||
alert.show({ type: 'error', message: 'No items selected.' });
|
||||
return;
|
||||
}
|
||||
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();
|
||||
|
||||
return await data.purchaseShoppingList(items);
|
||||
}
|
||||
const requestedSources = productGroupsPurchased.map(item => item.requested).flat();
|
||||
const completedRequests = getCompletedRequests(currentShoppingList, results);
|
||||
const servicedRequests = sourcesToRequests(requestedSources);
|
||||
|
||||
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;
|
||||
return await data.purchaseItems(storeName, servicedRequests, results, completedRequests);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
@ -175,74 +173,69 @@ export default {
|
|||
const to = new Date();
|
||||
to.setDate(to.getDate() + 7);
|
||||
|
||||
return { from, to, shoppingList: null, selected: [], showPurchased: false }
|
||||
return { from, to, availableMeals: [], purchasedMeals: [], shoppingList: null, includedMeals: [], productGroupsToPurchase: [], purchasedGroups: [], selected: [], showPurchased: false }
|
||||
},
|
||||
async beforeMount() {
|
||||
this.loadData();
|
||||
},
|
||||
computed: {
|
||||
outstandingItemGroups() {
|
||||
return itemsToGroups(this.shoppingList?.outstanding_items ?? []);
|
||||
watch: {
|
||||
shoppingList: {
|
||||
handler: 'updateLists',
|
||||
deep: true
|
||||
},
|
||||
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) {
|
||||
await data.requestMeal(meal.id);
|
||||
await this.loadData();
|
||||
const request = await data.requestMeal(meal.id);
|
||||
this.shoppingList.requests.push(request);
|
||||
},
|
||||
async mealUnselected(meal) {
|
||||
await data.unrequestMeal(meal.id);
|
||||
await this.loadData();
|
||||
this.shoppingList.requests = this.shoppingList.requests.filter(r => r.meal?.id !== meal.id);
|
||||
},
|
||||
async markFound() {
|
||||
await saveShoppingList(this.selected);
|
||||
await saveShoppingList('', this.shoppingList, this.selected);
|
||||
|
||||
this.selected = [];
|
||||
await this.loadData();
|
||||
this.loadData();
|
||||
},
|
||||
async markPurchased() {
|
||||
const shoppingList = await saveShoppingList(this.selected);
|
||||
|
||||
if (!shoppingList || !shoppingList.id) {
|
||||
alert.show({ type: 'error', message: 'Failed to purchase.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const shoppingList = await saveShoppingList('', this.shoppingList, this.selected);
|
||||
this.selected = [];
|
||||
this.$router.push(`/shopping/${shoppingList.id}`);
|
||||
},
|
||||
toggleSelect(item) {
|
||||
const index = this.selected.findIndex(i => groupsMatch(i, item));
|
||||
const index = this.selected.findIndex(i => i === item);
|
||||
if (index === -1)
|
||||
this.selected.push(item);
|
||||
else
|
||||
this.selected.splice(index, 1);
|
||||
},
|
||||
isSelected(item) {
|
||||
return this.selected.some(i => groupsMatch(i, item));
|
||||
return this.selected.some(i => i === item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
async updateShoppingList(save = false) {
|
||||
const new_ingredients = save ?
|
||||
const requests = save ?
|
||||
await data.saveMyShoppingList(this.ingredients) :
|
||||
await data.getMyShoppingList();
|
||||
|
||||
this.ingredients = new_ingredients;
|
||||
this.ingredients = requests.map(r => r.ingredient);
|
||||
},
|
||||
addIngredient() {
|
||||
this.ingredients = [{ id: -1 }, ...this.ingredients];
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<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>
|
||||
<h4>Included Meals</h4>
|
||||
<meal-selection-list :checked="includedMeals" :meals="includedMeals" :disabled="true" />
|
||||
|
||||
<ul class="full-shopping-list">
|
||||
<li v-for="item in listByProduct" :key="item.id">
|
||||
<shopping-list-item :shopping-list-item-group="item" />
|
||||
<shopping-list-item :productGrouping="item" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
|
@ -34,7 +32,7 @@
|
|||
import { ago } from '@/dateformats.js'
|
||||
|
||||
import data from '@/data.js'
|
||||
import { itemsToGroups } from './shopping.js'
|
||||
import { purchasedToProductModel } from './shopping.js'
|
||||
|
||||
import MealSelectionList from './MealSelectionList.vue'
|
||||
import ShoppingListItem from './ShoppingListItem.vue'
|
||||
|
|
@ -45,31 +43,17 @@ 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
|
||||
|
|
|
|||
|
|
@ -2,24 +2,21 @@
|
|||
|
||||
<!-- 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="`${ shoppingListItemGroup.product?.img_small ?? require('@/assets/missing-product.svg') }`" class="product-image" />
|
||||
<img :src="productGrouping.product.img_small" class="product-image" />
|
||||
<div class="product-details">
|
||||
<h3 class="header">
|
||||
<strong>
|
||||
<a v-if="shoppingListItemGroup.product?.link" :href="shoppingListItemGroup.product?.link">{{ shoppingListItemGroup.product?.name }}</a>
|
||||
<span v-else>{{ shoppingListItemGroup.name }}</span>
|
||||
</strong>,
|
||||
<strong><a :href="productGrouping.product.link">{{ productGrouping.product.name }}</a></strong>,
|
||||
<small>
|
||||
<span v-for="(total, index) in remainingRequiredTotals" :key="total.id">
|
||||
<span v-for="(total, index) in remainingRequired" :key="total.id">
|
||||
<span v-if="index">, </span>
|
||||
<span>{{ formatQuantity(total.quantity) }} {{ total.unit }}</span>
|
||||
</span>
|
||||
<span class="found-marker partial" v-if="purchased.length > 0">✓ {{ getFriendlyDate(lastPurchased) }}</span>
|
||||
<span class="found-marker partial" v-if="productGrouping.purchased.length > 0">✓ {{ getFriendlyDate(lastPurchased) }}</span>
|
||||
</small>
|
||||
</h3>
|
||||
<p class="sources" v-if="required.length > 0">
|
||||
<p class="sources" v-if="productGrouping.requested.length > 0">
|
||||
<strong>Need: </strong>
|
||||
<span v-for="(source, index) in required" :key="source.id">
|
||||
<span v-for="(source, index) in productGrouping.requested" :key="source.id">
|
||||
<span v-if="index">, and </span>
|
||||
<span v-if="source.person">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }}
|
||||
|
|
@ -34,9 +31,9 @@
|
|||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p v-if="purchased.length > 0">
|
||||
<p v-if="productGrouping.purchased.length > 0">
|
||||
<strong>Already found or purchased: </strong>
|
||||
<span v-for="(source, index) in purchased" :key="source.id">
|
||||
<span v-for="(source, index) in productGrouping.purchased" :key="source.id">
|
||||
<span v-if="index">, and </span>
|
||||
<span v-if="source.person">
|
||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }}
|
||||
|
|
@ -116,37 +113,23 @@ import { calculateTotals } from '@/units.js';
|
|||
|
||||
export default {
|
||||
name: 'ShoppingListItem',
|
||||
props: ['shoppingListItemGroup' ], // { product: { ... }, OR name: 'string', shoppingListItems: { person, ingredient, list_id?, meal? }} where list_id is null if not yet purchased
|
||||
props: ['productGrouping' ],
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
remainingRequiredTotals() {
|
||||
return calculateTotals(this.shoppingListItemGroup.shoppingListItems.map(item => item.ingredient));
|
||||
remainingRequired() {
|
||||
return calculateTotals(this.productGrouping.requested.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);
|
||||
expectedExisting() {
|
||||
return calculateTotals(this.productGrouping.purchased.map(r => r.ingredient));
|
||||
},
|
||||
lastPurchased() {
|
||||
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);
|
||||
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));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,151 @@
|
|||
export function groupsToItems(groups) {
|
||||
return groups.map(group => group.shoppingListItems).flat();
|
||||
function scaleFactor(mealRecipe) {
|
||||
const numRecipe = mealRecipe.recipe.serves;
|
||||
const numRequested = mealRecipe.servings;
|
||||
|
||||
return numRequested / numRecipe;
|
||||
}
|
||||
|
||||
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);
|
||||
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, }, });
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...Object.values(ingredients_by_product_id),
|
||||
...Object.values(ingredients_by_name)
|
||||
]
|
||||
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;
|
||||
}
|
||||
83
src/data.js
83
src/data.js
|
|
@ -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 => ({ 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 }) },
|
||||
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 }) },
|
||||
Recipe: { fields: [ "date_created", "date_hidden" ], },
|
||||
};
|
||||
|
||||
|
|
@ -43,39 +43,6 @@ 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 {
|
||||
|
|
@ -246,10 +213,10 @@ export default {
|
|||
},
|
||||
async getMyShoppingList() {
|
||||
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", { credentials: "include" });
|
||||
const ingredients = await response.json();
|
||||
fixDates(ingredients, 'Ingredient');
|
||||
const requests = await response.json();
|
||||
fixDates(requests, "ShoppingListRequest");
|
||||
|
||||
return ingredients;
|
||||
return requests;
|
||||
},
|
||||
async saveMyShoppingList(list) {
|
||||
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", {
|
||||
|
|
@ -261,44 +228,39 @@ export default {
|
|||
body: JSON.stringify(list),
|
||||
});
|
||||
|
||||
const ingredients = await response.json();
|
||||
fixDates(ingredients, 'Ingredient');
|
||||
const requests = await response.json();
|
||||
fixDates(requests, "ShoppingListRequest");
|
||||
|
||||
return ingredients;
|
||||
return requests;
|
||||
},
|
||||
async getShoppingList(id) {
|
||||
const response = await fetch(BASE_URL + `/shopping/${encodeURIComponent(id)}`);
|
||||
|
||||
const purchasedShoppingList = await response.json();
|
||||
fixDates(purchasedShoppingList, "PurchasedShoppingList");
|
||||
setPurchasedShoppingListReferences(purchasedShoppingList);
|
||||
|
||||
return purchasedShoppingList.list;
|
||||
const lst = await response.json();
|
||||
fixDates(lst, "ShoppingList");
|
||||
return lst;
|
||||
},
|
||||
async getCurrentShoppingList() {
|
||||
const response = await fetch(BASE_URL + "/shopping/current");
|
||||
|
||||
const lst = await response.json();
|
||||
fixDates(lst, "CurrentShoppingList");
|
||||
setCurrentShoppingListReferences(lst);
|
||||
|
||||
|
||||
return lst;
|
||||
},
|
||||
async purchaseShoppingList(completed_requests) {
|
||||
async purchaseItems(storeName, requests, results, completedRequests) {
|
||||
const response = await fetch(BASE_URL + "/shopping/", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ items: completed_requests }),
|
||||
body: JSON.stringify({ requests, results, store_name: storeName, completed_requests: completedRequests }),
|
||||
});
|
||||
|
||||
const purchasedShoppingList = await response.json();
|
||||
fixDates(purchasedShoppingList, "PurchasedShoppingList");
|
||||
setPurchasedShoppingListReferences(purchasedShoppingList);
|
||||
const lst = await response.json();
|
||||
fixDates(lst, "ShoppingList");
|
||||
|
||||
return purchasedShoppingList.list;
|
||||
return lst;
|
||||
},
|
||||
async requestMeal(meal_id) {
|
||||
const response = await fetch(BASE_URL + "/shopping/current/meals/me", {
|
||||
|
|
@ -311,7 +273,7 @@ export default {
|
|||
});
|
||||
|
||||
const requests = await response.json();
|
||||
fixDates(requests, "ShoppingListItem");
|
||||
fixDates(requests, "ShoppingListRequest");
|
||||
|
||||
return requests;
|
||||
},
|
||||
|
|
@ -321,9 +283,8 @@ export default {
|
|||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to unrequest meal");
|
||||
}
|
||||
const requests = await response.json();
|
||||
fixDates(requests, "ShoppingListRequest");
|
||||
},
|
||||
async getPersonsInHome() {
|
||||
const response = await fetch(BASE_URL + "/persons");
|
||||
|
|
|
|||
Loading…
Reference in a new issue