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" />
|
<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 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
20
src/data.js
20
src/data.js
|
|
@ -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
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