mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Feature/shopping list (#172)
* API Endpoint * shopping list added to the UI * fixed category sidebar on mobile Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
a56d4f6d11
commit
6ee9a893ef
8 changed files with 204 additions and 16 deletions
23
frontend/package-lock.json
generated
23
frontend/package-lock.json
generated
|
@ -5585,10 +5585,17 @@
|
|||
"integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM="
|
||||
},
|
||||
"fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npm.taobao.org/fast-levenshtein/download/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||
"dev": true
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz",
|
||||
"integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==",
|
||||
"requires": {
|
||||
"fastest-levenshtein": "^1.0.7"
|
||||
}
|
||||
},
|
||||
"fastest-levenshtein": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
|
||||
"integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow=="
|
||||
},
|
||||
"faye-websocket": {
|
||||
"version": "0.11.3",
|
||||
|
@ -8334,6 +8341,14 @@
|
|||
"prelude-ls": "~1.1.2",
|
||||
"type-check": "~0.3.2",
|
||||
"word-wrap": "~1.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ora": {
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@smartweb/vue-flash-message": "^0.6.10",
|
||||
"axios": "^0.21.1",
|
||||
"core-js": "^3.8.2",
|
||||
"fast-levenshtein": "^3.0.0",
|
||||
"fuse.js": "^6.4.6",
|
||||
"qs": "^6.9.6",
|
||||
"v-jsoneditor": "^1.4.2",
|
||||
|
|
|
@ -8,9 +8,10 @@ const mealPlanURLs = {
|
|||
all: `${prefix}all`,
|
||||
create: `${prefix}create`,
|
||||
thisWeek: `${prefix}this-week`,
|
||||
update: (planID) => `${prefix}${planID}`,
|
||||
delete: (planID) => `${prefix}${planID}`,
|
||||
update: planID => `${prefix}${planID}`,
|
||||
delete: planID => `${prefix}${planID}`,
|
||||
today: `${prefix}today`,
|
||||
shopping: planID => `${prefix}${planID}/shopping-list`,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -43,4 +44,9 @@ export default {
|
|||
let response = await apiReq.put(mealPlanURLs.update(id), body);
|
||||
return response;
|
||||
},
|
||||
|
||||
async shoppingList(id) {
|
||||
let response = await apiReq.get(mealPlanURLs.shopping(id));
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
112
frontend/src/components/MealPlan/ShoppingListDialog.vue
Normal file
112
frontend/src/components/MealPlan/ShoppingListDialog.vue
Normal file
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="650">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
Shopping List
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text color="accent" @click="group = !group">
|
||||
Group (Beta)
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text v-if="group == false">
|
||||
<v-list
|
||||
dense
|
||||
v-for="(recipe, index) in ingredients"
|
||||
:key="`${index}-recipe`"
|
||||
>
|
||||
<v-subheader>{{ recipe.name }} </v-subheader>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item
|
||||
v-for="(item, i) in recipe.recipeIngredient"
|
||||
:key="i"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else>
|
||||
<v-list dense>
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item v-for="(item, i) in rawIngredients" :key="i">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="item"></v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
const levenshtein = require("fast-levenshtein");
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
planID: 0,
|
||||
ingredients: [],
|
||||
rawIngredients: [],
|
||||
group: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
openDialog: function(id) {
|
||||
this.dialog = true;
|
||||
this.planID = id;
|
||||
this.getIngredients();
|
||||
},
|
||||
async getIngredients() {
|
||||
this.ingredients = await api.mealPlans.shoppingList(this.planID);
|
||||
console.log(this.ingredients);
|
||||
this.getRawIngredients();
|
||||
},
|
||||
getRawIngredients() {
|
||||
this.ingredients.forEach(element => {
|
||||
this.rawIngredients.push(element.recipeIngredient);
|
||||
});
|
||||
|
||||
this.rawIngredients = this.rawIngredients.flat();
|
||||
this.rawIngredients = this.levenshteinFilter(this.rawIngredients);
|
||||
console.log(this.rawIngredients);
|
||||
},
|
||||
levenshteinFilter(source, maximum = 5) {
|
||||
let _source, matches, x, y;
|
||||
_source = source.slice();
|
||||
matches = [];
|
||||
for (x = _source.length - 1; x >= 0; x--) {
|
||||
let output = _source.splice(x, 1);
|
||||
for (y = _source.length - 1; y >= 0; y--) {
|
||||
if (levenshtein.get(output[0], _source[y]) <= maximum) {
|
||||
output.push(_source[y]);
|
||||
_source.splice(y, 1);
|
||||
x--;
|
||||
}
|
||||
}
|
||||
matches.push(output);
|
||||
}
|
||||
return matches.flat();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,20 +1,42 @@
|
|||
<template>
|
||||
<v-navigation-drawer width="175px" clipped app :expand-on-hover="!mobile">
|
||||
<v-list nav dense>
|
||||
<v-list-item v-for="nav in links" :key="nav.title" link :to="nav.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title | titleCase }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<div>
|
||||
<v-btn
|
||||
class="mt-9 ml-n1"
|
||||
fixed
|
||||
left
|
||||
bottom
|
||||
fab
|
||||
small
|
||||
color="primary"
|
||||
@click="showSidebar = !showSidebar"
|
||||
>
|
||||
<v-icon>mdi-tag</v-icon></v-btn
|
||||
>
|
||||
|
||||
<v-navigation-drawer
|
||||
:value="mobile ? showSidebar : true"
|
||||
v-model="showSidebar"
|
||||
width="175px"
|
||||
clipped
|
||||
app
|
||||
>
|
||||
<v-list nav dense>
|
||||
<v-list-item v-for="nav in links" :key="nav.title" link :to="nav.to">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title | titleCase }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showSidebar: false,
|
||||
links: [],
|
||||
baseLinks: [
|
||||
{
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"sign-up": "Sign up"
|
||||
},
|
||||
"meal-plan": {
|
||||
"shopping-list": "Shopping List",
|
||||
"dinner-this-week": "Dinner This Week",
|
||||
"meal-planner": "Meal Planner",
|
||||
"dinner-today": "Dinner Today",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@updated="planUpdated"
|
||||
/>
|
||||
<NewMeal v-else @created="requestMeals" class="mb-5" />
|
||||
<ShoppingListDialog ref="shoppingList" />
|
||||
|
||||
<v-card class="my-2">
|
||||
<v-card-title class="headline">
|
||||
|
@ -49,6 +50,14 @@
|
|||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-card-actions class="mt-n5">
|
||||
<v-btn
|
||||
color="accent lighten-2"
|
||||
class="mx-0"
|
||||
text
|
||||
@click="openShoppingList(mealplan.uid)"
|
||||
>
|
||||
{{ $t("meal-plan.shopping-list") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="accent lighten-2"
|
||||
|
@ -78,11 +87,13 @@ import api from "@/api";
|
|||
import utils from "@/utils";
|
||||
import NewMeal from "../components/MealPlan/MealPlanNew";
|
||||
import EditPlan from "../components/MealPlan/MealPlanEditor";
|
||||
import ShoppingListDialog from "../components/MealPlan/ShoppingListDialog";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewMeal,
|
||||
EditPlan,
|
||||
ShoppingListDialog,
|
||||
},
|
||||
data: () => ({
|
||||
plannedMeals: [],
|
||||
|
@ -122,6 +133,10 @@ export default {
|
|||
api.mealPlans.delete(id);
|
||||
this.requestMeals();
|
||||
},
|
||||
openShoppingList(id) {
|
||||
console.log(this.$refs.shoppingList.openDialog);
|
||||
this.$refs.shoppingList.openDialog(id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from typing import List
|
||||
|
||||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from services.meal_services import MealPlan
|
||||
|
@ -16,6 +17,21 @@ def get_all_meals(session: Session = Depends(generate_session)):
|
|||
return MealPlan.get_all(session)
|
||||
|
||||
|
||||
@router.get("/{id}/shopping-list")
|
||||
def get_shopping_list(id: str, session: Session = Depends(generate_session)):
|
||||
|
||||
#! Refactor into Single Database Call
|
||||
mealplan = db.meals.get(session, id)
|
||||
slugs = [x.get("slug") for x in mealplan.get("meals")]
|
||||
recipes = [db.recipes.get(session, x) for x in slugs]
|
||||
ingredients = [
|
||||
{"name": x.get("name"), "recipeIngredient": x.get("recipeIngredient")}
|
||||
for x in recipes
|
||||
]
|
||||
|
||||
return ingredients
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
def set_meal_plan(data: MealPlan, session: Session = Depends(generate_session)):
|
||||
""" Creates a meal plan database entry """
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue