FIrst render
This commit is contained in:
parent
67cbb370ed
commit
ce3e255eb4
5 changed files with 133 additions and 217 deletions
|
|
@ -5,19 +5,19 @@
|
||||||
<meal-selection-list @meal-selected="mealSelected" @meal-unselected="mealUnselected" :checked="includedMeals" :meals="availableMeals" />
|
<meal-selection-list @meal-selected="mealSelected" @meal-unselected="mealUnselected" :checked="includedMeals" :meals="availableMeals" />
|
||||||
|
|
||||||
<ul class="full-shopping-list">
|
<ul class="full-shopping-list">
|
||||||
<li v-for="item in productGroupsToPurchase" :key="item.id" class="selectable" :class="{ 'selected': isSelected(item) }" @click="toggleSelect(item)">
|
<li v-for="group in outstandingItemGroups" :key="group.id" class="selectable" :class="{ 'selected': isSelected(group) }" @click="toggleSelect(group)">
|
||||||
<shopping-list-item :productGrouping="item" />
|
<shopping-list-item :shopping-list-item-group="group" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div v-if="productGroupsToPurchase.length === 0">
|
<div v-if="outstandingItemGroups.length === 0">
|
||||||
<p>
|
<p>
|
||||||
No items to purchase
|
No items to purchase
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="purchased-slider">
|
<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-if="showPurchased" @click="showPurchased=false" >⏶ Hide Purchased ⏶</button>
|
||||||
<button v-else @click="showPurchased=true">⏷ Show Purchased ⏷</button>
|
<button v-else @click="showPurchased=true">⏷ Show Purchased ⏷</button>
|
||||||
|
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
Purchased Items
|
Purchased Items
|
||||||
</h4>
|
</h4>
|
||||||
<ul class="full-shopping-list">
|
<ul class="full-shopping-list">
|
||||||
<li v-for="item in purchasedGroups" :key="item.id">
|
<li v-for="item in purchasedItemGroups" :key="item.id">
|
||||||
<shopping-list-item :productGrouping="item" />
|
<shopping-list-item :shopping-list-item-group="item" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -138,19 +138,19 @@ button img {
|
||||||
import alert from '@/alert.js'
|
import alert from '@/alert.js'
|
||||||
|
|
||||||
import data from '@/data.js'
|
import data from '@/data.js'
|
||||||
import { toProductsModel, } from './shopping.js'
|
import { itemsToGroups, groupsToItems } from './shopping.js'
|
||||||
|
|
||||||
import MealSelectionList from './MealSelectionList.vue'
|
import MealSelectionList from './MealSelectionList.vue'
|
||||||
import ShoppingListItem from './ShoppingListItem.vue'
|
import ShoppingListItem from './ShoppingListItem.vue'
|
||||||
|
|
||||||
async function saveShoppingList(productGroupsPurchased) {
|
async function saveShoppingList(outstandingItemGroups) {
|
||||||
const results = productGroupsPurchased.map(item => item.requested.map(r => ({
|
const items = groupsToItems(outstandingItemGroups);
|
||||||
ingredient_id: r.ingredient.id,
|
if (items.length === 0) {
|
||||||
person_id: r.person_id,
|
alert.show({ type: 'error', message: 'No items selected.' });
|
||||||
list_id: null,
|
return;
|
||||||
}))).flat();
|
}
|
||||||
|
|
||||||
return await data.purchaseItems(results);
|
return await data.purchaseItems(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -169,54 +169,50 @@ export default {
|
||||||
const to = new Date();
|
const to = new Date();
|
||||||
to.setDate(to.getDate() + 7);
|
to.setDate(to.getDate() + 7);
|
||||||
|
|
||||||
return { from, to, availableMeals: [], purchasedMeals: [], shoppingList: null, includedMeals: [], productGroupsToPurchase: [], purchasedGroups: [], selected: [], showPurchased: false }
|
return { from, to, shoppingList: null, includedMeals: [], selected: [], showPurchased: false }
|
||||||
},
|
},
|
||||||
async beforeMount() {
|
async beforeMount() {
|
||||||
this.loadData();
|
this.loadData();
|
||||||
},
|
},
|
||||||
watch: {
|
computed: {
|
||||||
shoppingList: {
|
outstandingItemGroups() {
|
||||||
handler: 'updateLists',
|
return itemsToGroups(this.shoppingList?.outstanding_items ?? []);
|
||||||
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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async loadData() {
|
async loadData() {
|
||||||
|
this.upcomingMeals = await data.getUpcomingMeals(this.from, this.to);
|
||||||
this.shoppingList = await data.getCurrentShoppingList();
|
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) {
|
async mealSelected(meal) {
|
||||||
const request = await data.requestMeal(meal.id);
|
await data.requestMeal(meal.id);
|
||||||
this.shoppingList.requests.push(request);
|
await this.loadData();
|
||||||
},
|
},
|
||||||
async mealUnselected(meal) {
|
async mealUnselected(meal) {
|
||||||
await data.unrequestMeal(meal.id);
|
await data.unrequestMeal(meal.id);
|
||||||
this.shoppingList.requests = this.shoppingList.requests.filter(r => r.meal?.id !== meal.id);
|
await this.loadData();
|
||||||
},
|
},
|
||||||
async markFound() {
|
async markFound() {
|
||||||
await saveShoppingList(this.selected);
|
await saveShoppingList(this.selected);
|
||||||
|
|
||||||
this.selected = [];
|
this.selected = [];
|
||||||
this.loadData();
|
await this.loadData();
|
||||||
},
|
},
|
||||||
async markPurchased() {
|
async markPurchased() {
|
||||||
const shoppingList = await saveShoppingList(this.selected);
|
const shoppingList = await saveShoppingList(this.selected);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
import { ago } from '@/dateformats.js'
|
import { ago } from '@/dateformats.js'
|
||||||
|
|
||||||
import data from '@/data.js'
|
import data from '@/data.js'
|
||||||
import { purchasedToProductModel } from './shopping.js'
|
import { itemsToGroups } from './shopping.js'
|
||||||
|
|
||||||
import MealSelectionList from './MealSelectionList.vue'
|
import MealSelectionList from './MealSelectionList.vue'
|
||||||
import ShoppingListItem from './ShoppingListItem.vue'
|
import ShoppingListItem from './ShoppingListItem.vue'
|
||||||
|
|
@ -52,8 +52,8 @@ export default {
|
||||||
},
|
},
|
||||||
async beforeMount() {
|
async beforeMount() {
|
||||||
this.shoppingList = await data.getShoppingList(this.id);
|
this.shoppingList = await data.getShoppingList(this.id);
|
||||||
this.includedMeals = this.shoppingList.requests.map(r => r.meal).filter(m => m);
|
this.includedMeals = Object.values(this.shoppingList.meal_lookup);
|
||||||
this.listByProduct = purchasedToProductModel(this.shoppingList);
|
this.listByProduct = itemsToGroups(this.shoppingList);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
ago
|
ago
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
<!-- 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">
|
<div class="shopping-list-item">
|
||||||
<img :src="`${ productGrouping.product?.img_small ?? require('@/assets/missing-product.svg') }`" class="product-image" />
|
<img :src="`${ shoppingListItemGroup.product?.img_small ?? require('@/assets/missing-product.svg') }`" class="product-image" />
|
||||||
<div class="product-details">
|
<div class="product-details">
|
||||||
<h3 class="header">
|
<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>
|
<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 v-if="index">, </span>
|
||||||
<span>{{ formatQuantity(total.quantity) }} {{ total.unit }}</span>
|
<span>{{ formatQuantity(total.quantity) }} {{ total.unit }}</span>
|
||||||
</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>
|
</small>
|
||||||
</h3>
|
</h3>
|
||||||
<p class="sources" v-if="productGrouping.requested.length > 0">
|
<p class="sources" v-if="required.length > 0">
|
||||||
<strong>Need: </strong>
|
<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="index">, and </span>
|
||||||
<span v-if="source.person">
|
<span v-if="source.person">
|
||||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }}
|
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }}
|
||||||
|
|
@ -31,9 +34,9 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="productGrouping.purchased.length > 0">
|
<p v-if="purchased.length > 0">
|
||||||
<strong>Already found or purchased: </strong>
|
<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="index">, and </span>
|
||||||
<span v-if="source.person">
|
<span v-if="source.person">
|
||||||
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }}
|
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for {{ source.person.name }}
|
||||||
|
|
@ -113,23 +116,37 @@ import { calculateTotals } from '@/units.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ShoppingListItem',
|
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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
remainingRequired() {
|
remainingRequiredTotals() {
|
||||||
return calculateTotals(this.productGrouping.requested.map(r => r.ingredient));
|
return calculateTotals(this.shoppingListItemGroup.shoppingListItems.map(item => item.ingredient));
|
||||||
},
|
},
|
||||||
expectedExisting() {
|
expectedExistingTotals() {
|
||||||
return calculateTotals(this.productGrouping.purchased.map(r => r.ingredient));
|
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() {
|
lastPurchased() {
|
||||||
return this.productGrouping.purchased.reduce((latest, source) => {
|
const purchasedItems = this.shoppingListItemGroup.shoppingListItems.filter(item => item.list_id);
|
||||||
return (source.shop.created_date && latest > source.shop.created_date) ? latest : source.shop.created_date;
|
if (purchasedItems.length === 0) {
|
||||||
}, new Date(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: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -1,158 +1,35 @@
|
||||||
function scaleFactor(mealRecipe) {
|
export function groupsToItems(groups) {
|
||||||
const numRecipe = mealRecipe.recipe.serves;
|
return groups.map(group => group.shoppingListItems).flat();
|
||||||
const numRequested = mealRecipe.servings;
|
|
||||||
|
|
||||||
return numRequested / numRecipe;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mealToShoppingListSources(meal) {
|
export function itemsToGroups(shoppingListItems) {
|
||||||
const sources = [];
|
const ingredients_by_product_id = {};
|
||||||
for (const mealRecipe of meal.recipes) {
|
const ingredients_by_name = {};
|
||||||
const scale = scaleFactor(mealRecipe);
|
for (const item of shoppingListItems) {
|
||||||
for (const ingredient of mealRecipe.recipe.ingredients) {
|
if (item.ingredient.product) {
|
||||||
const quantity = ingredient.quantity * scale;
|
let group = ingredients_by_product_id[item.ingredient.product.id];
|
||||||
sources.push({ meal, mealRecipe, recipe: mealRecipe.recipe, ingredient: { ...ingredient, quantity, }, });
|
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) {
|
return [
|
||||||
sources.push({ ingredient, meal, });
|
...Object.values(ingredients_by_product_id),
|
||||||
}
|
...Object.values(ingredients_by_name)
|
||||||
|
]
|
||||||
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: [], };
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ?? 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;
|
|
||||||
}
|
}
|
||||||
34
src/data.js
34
src/data.js
|
|
@ -2,9 +2,9 @@ const BASE_URL = window.location.href.replace(/^(https?:\/\/[^/]+).*/, "$1/api")
|
||||||
|
|
||||||
const datesToFix = {
|
const datesToFix = {
|
||||||
Meal: { fields: [ "suggested_date", "purchase_date", "consumed_date" ] },
|
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 }) },
|
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" ], },
|
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;
|
let user = null;
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -212,8 +232,10 @@ export default {
|
||||||
},
|
},
|
||||||
async getMyShoppingList() {
|
async getMyShoppingList() {
|
||||||
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", { credentials: "include" });
|
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", { credentials: "include" });
|
||||||
|
const ingredients = await response.json();
|
||||||
|
fixDates(ingredients, 'Ingredient');
|
||||||
|
|
||||||
return await response.json();
|
return ingredients;
|
||||||
},
|
},
|
||||||
async saveMyShoppingList(list) {
|
async saveMyShoppingList(list) {
|
||||||
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", {
|
const response = await fetch(BASE_URL + "/shopping/current/me/ingredients", {
|
||||||
|
|
@ -225,7 +247,10 @@ export default {
|
||||||
body: JSON.stringify(list),
|
body: JSON.stringify(list),
|
||||||
});
|
});
|
||||||
|
|
||||||
return await response.json();
|
const ingredients = await response.json();
|
||||||
|
fixDates(ingredients, 'Ingredient');
|
||||||
|
|
||||||
|
return ingredients;
|
||||||
},
|
},
|
||||||
async getShoppingList(id) {
|
async getShoppingList(id) {
|
||||||
const response = await fetch(BASE_URL + `/shopping/${encodeURIComponent(id)}`);
|
const response = await fetch(BASE_URL + `/shopping/${encodeURIComponent(id)}`);
|
||||||
|
|
@ -238,6 +263,7 @@ export default {
|
||||||
const response = await fetch(BASE_URL + "/shopping/current");
|
const response = await fetch(BASE_URL + "/shopping/current");
|
||||||
const lst = await response.json();
|
const lst = await response.json();
|
||||||
fixDates(lst, "CurrentShoppingList");
|
fixDates(lst, "CurrentShoppingList");
|
||||||
|
setShoppingListReferences(lst);
|
||||||
|
|
||||||
return lst;
|
return lst;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue