Added unit conversion and "Found" feature

This commit is contained in:
jableader 2024-05-19 13:43:59 +10:00
parent 69d99cfc9d
commit 36f7a24cbf
5 changed files with 203 additions and 8 deletions

View file

@ -5,7 +5,9 @@
<ul class="full-shopping-list"> <ul class="full-shopping-list">
<li v-for="item in listByProduct" :key="item.id" > <li v-for="item in listByProduct" :key="item.id" >
<shopping-list-item :product="item.product" :sources="item.sources" /> <shopping-list-item :product="item.product" :sources="item.sources" :totals="item.totals" />
<button v-if="item.product.id in found" @click="markNotFound(item)">Mark Not Found</button>
<button v-else @click="markFound(item)">Mark Found</button>
</li> </li>
</ul> </ul>
</template> </template>
@ -29,7 +31,7 @@
<script> <script>
import data from '@/data.js' import data from '@/data.js'
import { requestsToSources, groupByProduct } from './shopping.js' import { requestsToSources as flattenToIngredients, groupByProduct, groupingToIngredients } from './shopping.js'
import MealSelectionList from './MealSelectionList.vue' import MealSelectionList from './MealSelectionList.vue'
import ShoppingListItem from './ShoppingListItem.vue' import ShoppingListItem from './ShoppingListItem.vue'
@ -47,7 +49,7 @@ export default {
const to = new Date(); const to = new Date();
to.setDate(to.getDate() + 7); to.setDate(to.getDate() + 7);
return { from, to, availableMeals: [], shoppingList: null, includedMeals: [], listByProduct: []} return { from, to, availableMeals: [], shoppingList: null, includedMeals: [], listByProduct: [], found: {} }
}, },
async beforeMount() { async beforeMount() {
this.availableMeals = await data.getMeals(this.from, this.to); this.availableMeals = await data.getMeals(this.from, this.to);
@ -65,8 +67,14 @@ export default {
return; return;
this.includedMeals = this.shoppingList.requests.map(r => r.meal).filter(m => m); this.includedMeals = this.shoppingList.requests.map(r => r.meal).filter(m => m);
const sources = requestsToSources(this.shoppingList.requests);
const sources = flattenToIngredients(this.shoppingList.requests);
this.listByProduct = groupByProduct(sources); this.listByProduct = groupByProduct(sources);
this.found = this.shoppingList.results.reduce((acc, r) => {
acc[r.product.id] = r;
return acc;
}, {});
}, },
async mealSelected(meal) { async mealSelected(meal) {
const request = await data.requestMeal(meal.id); const request = await data.requestMeal(meal.id);
@ -75,6 +83,17 @@ export default {
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); this.shoppingList.requests = this.shoppingList.requests.filter(r => r.meal?.id !== meal.id);
},
async markFound(productGrouping) {
const result = await data.markFound(groupingToIngredients(productGrouping));
// Update shoppingList.result from result.created and result.removed
this.shoppingList.results = this.shoppingList.results.filter(r => !result.removed.some(removed => removed.id === r.id));
this.shoppingList.results.push(...result.created);
},
async markNotFound(item) {
const deletedRequests = await data.markNotFound(item.product.id);
this.shoppingList.results = this.shoppingList.results.filter(r => !deletedRequests.some(deleted => deleted.id === r.id));
} }
} }
} }

View file

@ -5,7 +5,13 @@
<img :src="product.img_small" class="product-image" /> <img :src="product.img_small" class="product-image" />
<div class="product-details"> <div class="product-details">
<h3 class="header"> <h3 class="header">
<strong><a :href="product.link">{{ product.name }}</a></strong>, <small>{{ quantityRequired }} required.</small> <strong><a :href="product.link">{{ product.name }}</a></strong>,
<small>
<span v-for="(total, index) in totals" :key="total.id">
<span v-if="index">, </span>
<span>{{ total.quantity }} {{ total.unit }}</span>
</span>
</small>
</h3> </h3>
<p class="sources"> <p class="sources">
<span v-for="(source, index) in sources" :key="source.id"> <span v-for="(source, index) in sources" :key="source.id">
@ -62,11 +68,10 @@
export default { export default {
name: 'ShoppingListItem', name: 'ShoppingListItem',
props: ['product', 'sources'], props: ['product', 'sources', 'totals'],
data() { data() {
return { return {
expanded: false, expanded: false,
quantityRequired: 5,
} }
}, },
} }

View file

@ -1,3 +1,5 @@
import units from '@/units.js';
export function mealToShoppingListSources(meal) { export function mealToShoppingListSources(meal) {
const sources = []; const sources = [];
for (const recipe of meal.recipes) { for (const recipe of meal.recipes) {
@ -31,6 +33,37 @@ export function removeMealFromShoppingListSources(sources, meal) {
return sources.filter(source => source.meal?.id !== meal.id); return sources.filter(source => source.meal?.id !== meal.id);
} }
export function groupingToIngredients(grouping) {
const product = grouping.product;
const totals = [];
for (const total of grouping.totals) {
totals.push({ id: 0, line: `${total.quantity} ${total.unit} of ${product.name}`, preparation: '', name: product.name, product, ...total });
}
return totals;
}
function calculateTotals(sources) {
const totals = {};
for (const source of sources) {
const { unit, factor } = units.getConversionFactor(source.ingredient.unit) ?? { unit: source.ingredient.unit, factor: 1 };
const quantity = source.ingredient.quantity / factor;
if (!totals[unit]) {
totals[unit] = quantity;
}
else {
totals[unit] += quantity;
}
}
const result = [];
for (const unit in totals) {
result.push({ unit, quantity: totals[unit] });
}
return result;
}
export function groupByProduct(sources) { export function groupByProduct(sources) {
const noProductKey = "no-product"; const noProductKey = "no-product";
const noProduct = { name: "No product", }; const noProduct = { name: "No product", };
@ -52,5 +85,9 @@ export function groupByProduct(sources) {
grouped[key].sources.push(source); grouped[key].sources.push(source);
} }
for (const key in grouped) {
grouped[key].totals = calculateTotals(grouped[key].sources);
}
return Object.values(grouped); return Object.values(grouped);
} }

View file

@ -186,6 +186,26 @@ export default {
credentials: "include", credentials: "include",
}); });
return await response.json();
},
async markFound(ingredients) {
const response = await fetch(BASE_URL + "/shopping/current/found", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(ingredients),
});
return await response.json();
},
async markNotFound(product_id) {
const response = await fetch(BASE_URL + `/shopping/current/found/${product_id}`, {
method: "DELETE",
credentials: "include",
});
return await response.json(); return await response.json();
} }
} }

114
src/units.js Normal file
View file

@ -0,0 +1,114 @@
export const equivalentUnits = {
'kg': {
'kgs': 1,
'kilograms': 1,
'kilogram': 1,
'g': 1000,
'gram': 1000,
'grams': 1000,
'lb': 2.20462,
'lbs': 2.20462,
'pound': 2.20462,
'pounds': 2.20462,
},
'litres': {
'l': 1,
'liter': 1,
'ml': 1000,
'milliliters': 1000,
'milliliter': 1000,
'fl oz': 33.814,
'fluid ounce': 33.814,
'fluid ounces': 33.814,
'cup': 4.22675,
'cups': 4.22675,
'tbsp': 67.628,
'tablespoon': 67.628,
'tablespoons': 67.628,
'tsp': 202.884,
'teaspoon': 202.884,
'teaspoons': 202.884,
'pt': 2.11338,
'pint': 2.11338,
'pints': 2.11338,
'qt': 1.05669,
'quart': 1.05669,
'quarts': 1.05669,
'gal': 0.264172,
'gallon': 0.264172,
'gallons': 0.264172,
'oz': 35.1951,
'ounce': 35.1951,
},
'items': {
'item': 1,
'items': 1,
'pcs': 1,
'piece': 1,
'pieces': 1,
'florets': 8, // Broccoli
'head': 1, // Broccoli
'heads': 1, // Broccoli
'slice': 10, // Bread
'slices': 10, // Bread
'loaf': 1, // Bread
'loaves': 1, // Bread
'cloves': 8, // Garlic
'bulb': 1, // Garlic
'bulbs': 1, // Garlic
'stalk': 1, // Celery
'stalks': 1, // Celery
'bunch': 1, // Cilantro
'bunches': 1, // Cilantro
'sprig': 1, // Cilantro
'sprigs': 1, // Cilantro
'cans': 1, // Canned goods
'can': 1, // Canned goods
'pack': 1, // Packaged goods
'packs': 1, // Packaged goods
'package': 1, // Packaged goods
'packages': 1, // Packaged goods
'container': 1, // Packaged goods
'containers': 1, // Packaged goods
},
}
function getBaseUnit(unit) {
for (const unitType in equivalentUnits) {
if (unit in equivalentUnits[unitType]) {
return unitType;
}
}
return null;
}
export default {
getConversionFactor(unit) {
let baseUnit = getBaseUnit(unit);
if (!baseUnit) {
unit = unit.toLowerCase();
baseUnit = getBaseUnit(unit);
}
if (!baseUnit) {
return null;
}
return {
unit: baseUnit,
factor: equivalentUnits[baseUnit][unit],
};
},
}