Added unit conversion and "Found" feature
This commit is contained in:
parent
69d99cfc9d
commit
36f7a24cbf
5 changed files with 203 additions and 8 deletions
|
|
@ -4,8 +4,10 @@
|
|||
<meal-selection-list @meal-selected="mealSelected" @meal-unselected="mealUnselected" :checked="includedMeals" :meals="availableMeals" />
|
||||
|
||||
<ul class="full-shopping-list">
|
||||
<li v-for="item in listByProduct" :key="item.id">
|
||||
<shopping-list-item :product="item.product" :sources="item.sources" />
|
||||
<li v-for="item in listByProduct" :key="item.id" >
|
||||
<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>
|
||||
</ul>
|
||||
</template>
|
||||
|
|
@ -29,7 +31,7 @@
|
|||
<script>
|
||||
|
||||
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 ShoppingListItem from './ShoppingListItem.vue'
|
||||
|
|
@ -47,7 +49,7 @@ export default {
|
|||
const to = new Date();
|
||||
to.setDate(to.getDate() + 7);
|
||||
|
||||
return { from, to, availableMeals: [], shoppingList: null, includedMeals: [], listByProduct: []}
|
||||
return { from, to, availableMeals: [], shoppingList: null, includedMeals: [], listByProduct: [], found: {} }
|
||||
},
|
||||
async beforeMount() {
|
||||
this.availableMeals = await data.getMeals(this.from, this.to);
|
||||
|
|
@ -65,8 +67,14 @@ export default {
|
|||
return;
|
||||
|
||||
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.found = this.shoppingList.results.reduce((acc, r) => {
|
||||
acc[r.product.id] = r;
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
async mealSelected(meal) {
|
||||
const request = await data.requestMeal(meal.id);
|
||||
|
|
@ -75,6 +83,17 @@ export default {
|
|||
async mealUnselected(meal) {
|
||||
await data.unrequestMeal(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@
|
|||
<img :src="product.img_small" class="product-image" />
|
||||
<div class="product-details">
|
||||
<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>
|
||||
<p class="sources">
|
||||
<span v-for="(source, index) in sources" :key="source.id">
|
||||
|
|
@ -62,11 +68,10 @@
|
|||
|
||||
export default {
|
||||
name: 'ShoppingListItem',
|
||||
props: ['product', 'sources'],
|
||||
props: ['product', 'sources', 'totals'],
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
quantityRequired: 5,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import units from '@/units.js';
|
||||
|
||||
export function mealToShoppingListSources(meal) {
|
||||
const sources = [];
|
||||
for (const recipe of meal.recipes) {
|
||||
|
|
@ -31,6 +33,37 @@ export function removeMealFromShoppingListSources(sources, meal) {
|
|||
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) {
|
||||
const noProductKey = "no-product";
|
||||
const noProduct = { name: "No product", };
|
||||
|
|
@ -52,5 +85,9 @@ export function groupByProduct(sources) {
|
|||
grouped[key].sources.push(source);
|
||||
}
|
||||
|
||||
for (const key in grouped) {
|
||||
grouped[key].totals = calculateTotals(grouped[key].sources);
|
||||
}
|
||||
|
||||
return Object.values(grouped);
|
||||
}
|
||||
20
src/data.js
20
src/data.js
|
|
@ -186,6 +186,26 @@ export default {
|
|||
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();
|
||||
}
|
||||
}
|
||||
114
src/units.js
Normal file
114
src/units.js
Normal 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],
|
||||
};
|
||||
},
|
||||
}
|
||||
Loading…
Reference in a new issue