mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-02 04:39:54 -05:00
Fix after rebase
This commit is contained in:
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
|
||||
<generic-modal-form v-if="this_model"
|
||||
:model="this_model"
|
||||
:action="this_action"
|
||||
:item1="this_item"
|
||||
:item2="this_target"
|
||||
:show="show_modal"
|
||||
@finish-action="finishAction"/>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
</div>
|
||||
<div class="col-xl-8 col-12">
|
||||
<div class="container-fluid d-flex flex-column flex-grow-1">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6" style="margin-top: 1vh">
|
||||
<h3>
|
||||
<!-- <model-menu/> Replace with a List Menu or a Checklist Menu? -->
|
||||
<span>{{ this.this_model.name }}</span>
|
||||
<span><b-button variant="link" @click="startAction({'action':'new'})"><i
|
||||
class="fas fa-plus-circle fa-2x"></i></b-button></span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
this is where shopping list items go
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import {ApiMixin} from "@/utils/utils";
|
||||
import {StandardToasts, ToastMixin} from "@/utils/utils";
|
||||
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||
// or i'm capturing it incorrectly
|
||||
name: 'ModelListView',
|
||||
mixins: [ApiMixin, ToastMixin],
|
||||
components: {GenericModalForm},
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
items: [],
|
||||
this_model: undefined,
|
||||
model_menu: undefined,
|
||||
this_action: undefined,
|
||||
this_item: {},
|
||||
show_modal: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// value is passed from lists.py
|
||||
let model_config = JSON.parse(document.getElementById('model_config').textContent)
|
||||
this.this_model = this.Models[model_config?.model]
|
||||
},
|
||||
methods: {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
startAction: function (e, param) {
|
||||
let source = e?.source ?? {}
|
||||
this.this_item = source
|
||||
// remove recipe from shopping list
|
||||
// mark on-hand
|
||||
// mark puchased
|
||||
// edit shopping category on food
|
||||
// delete food from shopping list
|
||||
// add food to shopping list
|
||||
// add other to shopping list
|
||||
// edit unit conversion
|
||||
// edit purchaseable unit
|
||||
switch (e.action) {
|
||||
case 'delete':
|
||||
this.this_action = this.Actions.DELETE
|
||||
this.show_modal = true
|
||||
break;
|
||||
case 'new':
|
||||
this.this_action = this.Actions.CREATE
|
||||
this.show_modal = true
|
||||
break;
|
||||
case 'edit':
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.UPDATE
|
||||
this.show_modal = true
|
||||
break;
|
||||
}
|
||||
},
|
||||
finishAction: function (e) {
|
||||
let update = undefined
|
||||
switch (e?.action) {
|
||||
case 'save':
|
||||
this.saveThis(e.form_data)
|
||||
break;
|
||||
}
|
||||
if (e !== 'cancel') {
|
||||
switch (this.this_action) {
|
||||
case this.Actions.DELETE:
|
||||
this.deleteThis(this.this_item.id)
|
||||
break;
|
||||
case this.Actions.CREATE:
|
||||
this.saveThis(e.form_data)
|
||||
break;
|
||||
case this.Actions.UPDATE:
|
||||
update = e.form_data
|
||||
update.id = this.this_item.id
|
||||
this.saveThis(update)
|
||||
break;
|
||||
case this.Actions.MERGE:
|
||||
this.mergeThis(this.this_item, e.form_data.target, false)
|
||||
break;
|
||||
case this.Actions.MOVE:
|
||||
this.moveThis(this.this_item.id, e.form_data.target.id)
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.clearState()
|
||||
},
|
||||
getItems: function (params) {
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params).then((results) => {
|
||||
if (results?.length) {
|
||||
this.items = results.data
|
||||
} else {
|
||||
console.log('no data returned')
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
})
|
||||
},
|
||||
getThis: function (id) {
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, {'id': id})
|
||||
},
|
||||
saveThis: function (thisItem) {
|
||||
if (!thisItem?.id) { // if there is no item id assume it's a new item
|
||||
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
|
||||
// this.items = result.data - refresh the list here
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
} else {
|
||||
this.genericAPI(this.this_model, this.Actions.UPDATE, thisItem).then((result) => {
|
||||
// this.refreshThis(thisItem.id) refresh the list here
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
}).catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
}
|
||||
},
|
||||
getRecipe: function (item) {
|
||||
// change to get pop up card? maybe same for unit and food?
|
||||
},
|
||||
deleteThis: function (id) {
|
||||
this.genericAPI(this.this_model, this.Actions.DELETE, {'id': id}).then((result) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
})
|
||||
},
|
||||
clearState: function () {
|
||||
this.show_modal = false
|
||||
this.this_action = undefined
|
||||
this.this_item = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,18 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import App from './ChecklistView'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
|
||||
let publicPath = localStorage.STATIC_URL + 'vue/'
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
publicPath = 'http://localhost:8080/'
|
||||
}
|
||||
export default __webpack_public_path__ = publicPath // eslint-disable-line
|
||||
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
@@ -61,10 +61,10 @@ import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import CookbookSlider from "@/components/CookbookSlider";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import {StandardToasts} from "@/utils/utils";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
import CookbookSlider from "../../components/CookbookSlider";
|
||||
import LoadingSpinner from "../../components/LoadingSpinner";
|
||||
import {StandardToasts} from "../../utils/utils";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,14 +25,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
||||
<b-form-checkbox
|
||||
v-model="show_split"
|
||||
name="check-button"
|
||||
v-if="paginated"
|
||||
class="shadow-none"
|
||||
style="position: relative; top: 50%; transform: translateY(-50%)"
|
||||
switch
|
||||
>
|
||||
<b-form-checkbox v-model="show_split" name="check-button" v-if="paginated" class="shadow-none" style="position: relative; top: 50%; transform: translateY(-50%)" switch>
|
||||
{{ $t("show_split_screen") }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
@@ -42,46 +35,19 @@
|
||||
<div class="col" :class="{ 'col-md-6': show_split }">
|
||||
<!-- model isn't paginated and loads in one API call -->
|
||||
<div v-if="!paginated">
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_left"
|
||||
v-bind:key="i.id"
|
||||
:item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"
|
||||
/>
|
||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i" :model="this_model" @item-action="startAction($event, 'left')" @finish-action="finishAction" />
|
||||
</div>
|
||||
<!-- model is paginated and needs managed -->
|
||||
<generic-infinite-cards v-if="paginated" :card_counts="left_counts" :scroll="show_split" @search="getItems($event, 'left')" @reset="resetList('left')">
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_left"
|
||||
v-bind:key="i.id"
|
||||
:item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"
|
||||
/>
|
||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id" :item="i" :model="this_model" @item-action="startAction($event, 'left')" @finish-action="finishAction" />
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
<div class="col col-md-6" v-if="show_split">
|
||||
<generic-infinite-cards
|
||||
v-if="this_model"
|
||||
:card_counts="right_counts"
|
||||
:scroll="show_split"
|
||||
@search="getItems($event, 'right')"
|
||||
@reset="resetList('right')"
|
||||
>
|
||||
<generic-infinite-cards v-if="this_model" :card_counts="right_counts" :scroll="show_split" @search="getItems($event, 'right')" @reset="resetList('right')">
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_right"
|
||||
v-bind:key="i.id"
|
||||
:item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'right')"
|
||||
@finish-action="finishAction"
|
||||
/>
|
||||
<generic-horizontal-card v-for="i in items_right" v-bind:key="i.id" :item="i" :model="this_model" @item-action="startAction($event, 'right')" @finish-action="finishAction" />
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
@@ -98,13 +64,12 @@ import { BootstrapVue } from "bootstrap-vue"
|
||||
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import { CardMixin, ApiMixin, getConfig } from "@/utils/utils"
|
||||
import { StandardToasts, ToastMixin } from "@/utils/utils"
|
||||
import { CardMixin, ApiMixin, getConfig, StandardToasts, getUserPreference, makeToast } from "@/utils/utils"
|
||||
|
||||
import GenericInfiniteCards from "@/components/GenericInfiniteCards"
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard"
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm"
|
||||
import ModelMenu from "@/components/ModelMenu"
|
||||
import ModelMenu from "@/components/ContextMenu/ModelMenu"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
//import StorageQuota from "@/components/StorageQuota";
|
||||
|
||||
@@ -114,13 +79,8 @@ export default {
|
||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||
// or i'm capturing it incorrectly
|
||||
name: "ModelListView",
|
||||
mixins: [CardMixin, ApiMixin, ToastMixin],
|
||||
components: {
|
||||
GenericHorizontalCard,
|
||||
GenericModalForm,
|
||||
GenericInfiniteCards,
|
||||
ModelMenu,
|
||||
},
|
||||
mixins: [CardMixin, ApiMixin],
|
||||
components: { GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu },
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
@@ -236,6 +196,7 @@ export default {
|
||||
}
|
||||
},
|
||||
finishAction: function (e) {
|
||||
let update = undefined
|
||||
switch (e?.action) {
|
||||
case "save":
|
||||
this.saveThis(e.form_data)
|
||||
@@ -263,7 +224,7 @@ export default {
|
||||
}
|
||||
this.clearState()
|
||||
},
|
||||
getItems: function (params, col) {
|
||||
getItems: function (params = {}, col) {
|
||||
let column = col || "left"
|
||||
params.options = { query: { extended: 1 } } // returns extended values in API response
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params)
|
||||
|
||||
@@ -629,7 +629,6 @@ export default {
|
||||
|
||||
apiFactory.updateRecipe(this.recipe_id, this.recipe,
|
||||
{}).then((response) => {
|
||||
console.log(response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
this.recipe_changed = false
|
||||
if (view_after) {
|
||||
|
||||
@@ -1,90 +1,61 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<template v-if="loading">
|
||||
<loading-spinner></loading-spinner>
|
||||
</template>
|
||||
<div id="app">
|
||||
<template v-if="loading">
|
||||
<loading-spinner></loading-spinner>
|
||||
</template>
|
||||
|
||||
<div v-if="!loading">
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{{ recipe.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row text-center">
|
||||
<div class="col col-md-12">
|
||||
<recipe-rating :recipe="recipe"></recipe-rating>
|
||||
<last-cooked :recipe="recipe" class="mt-2"></last-cooked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-auto">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<i>{{ recipe.description }}</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<keywords-component :recipe="recipe"></keywords-component>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col col-md-3">
|
||||
<div class="row d-flex" style="padding-left: 16px">
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<i class="fas fa-user-clock fa-2x text-primary"></i>
|
||||
<div v-if="!loading">
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{{ recipe.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<span class="text-primary"><b>{{ $t('Preparation') }}</b></span><br/>
|
||||
{{ recipe.working_time }} {{ $t('min') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-3">
|
||||
<div class="row d-flex">
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<i class="far fa-clock fa-2x text-primary"></i>
|
||||
<div class="row text-center">
|
||||
<div class="col col-md-12">
|
||||
<recipe-rating :recipe="recipe"></recipe-rating>
|
||||
<last-cooked :recipe="recipe" class="mt-2"></last-cooked>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<span class="text-primary"><b>{{ $t('Waiting') }}</b></span><br/>
|
||||
{{ recipe.waiting_time }} {{ $t('min') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-4 col-10 mt-2 mt-md-0 mt-lg-0 mt-xl-0">
|
||||
<div class="row d-flex" style="padding-left: 16px">
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<i class="fas fa-pizza-slice fa-2x text-primary"></i>
|
||||
<div class="my-auto">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<i>{{ recipe.description }}</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<input
|
||||
style="text-align: right; border-width:0px;border:none; padding:0px; padding-left: 0.5vw; padding-right: 8px; max-width: 80px"
|
||||
value="1" maxlength="3" min="0"
|
||||
type="number" class="form-control form-control-lg" v-model.number="servings"/>
|
||||
</div>
|
||||
<div class="my-auto ">
|
||||
<span class="text-primary"><b><template v-if="recipe.servings_text === ''">{{ $t('Servings') }}</template><template
|
||||
v-else>{{ recipe.servings_text }}</template></b></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-2 col-2 my-auto" style="text-align: right; padding-right: 1vw">
|
||||
<recipe-context-menu v-bind:recipe="recipe" :servings="servings"></recipe-context-menu>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div style="text-align: center">
|
||||
<keywords :recipe="recipe"></keywords>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 order-md-1 col-sm-12 order-sm-2 col-12 order-2" v-if="recipe && ingredient_count > 0">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col col-md-8">
|
||||
<h4 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t('Ingredients') }}</h4>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col col-md-3">
|
||||
<div class="row d-flex" style="padding-left: 16px">
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<i class="fas fa-user-clock fa-2x text-primary"></i>
|
||||
</div>
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<span class="text-primary"
|
||||
><b>{{ $t("Preparation") }}</b></span
|
||||
><br />
|
||||
{{ recipe.working_time }} {{ $t("min") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-3">
|
||||
<div class="row d-flex">
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<i class="far fa-clock fa-2x text-primary"></i>
|
||||
</div>
|
||||
<div class="my-auto" style="padding-right: 4px">
|
||||
<span class="text-primary"
|
||||
><b>{{ $t("Waiting") }}</b></span
|
||||
><br />
|
||||
{{ recipe.waiting_time }} {{ $t("min") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
@@ -94,184 +65,186 @@
|
||||
<template v-if="s.show_as_header && s.name !== '' && s.ingredients.length > 0">
|
||||
<b v-bind:key="s.id">{{s.name}}</b>
|
||||
</template>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm">
|
||||
<template v-for="i in s.ingredients" :key="i.id">
|
||||
<ingredient-component :ingredient="i" :ingredient_factor="ingredient_factor"
|
||||
@checked-state-changed="updateIngredientCheckedState"></ingredient-component>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-2 col-2 my-auto" style="text-align: right; padding-right: 1vw">
|
||||
<recipe-context-menu v-bind:recipe="recipe" :servings="servings"></recipe-context-menu>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-6 order-md-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div class="col-md-8 order-md-1 col-sm-12 order-sm-2 col-12 order-2" v-if="recipe && ingredient_count > 0">
|
||||
<ingredients-card
|
||||
:steps="recipe.steps"
|
||||
:recipe="recipe.id"
|
||||
:ingredient_factor="ingredient_factor"
|
||||
:servings="servings"
|
||||
:header="true"
|
||||
@checked-state-changed="updateIngredientCheckedState"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img class="img img-fluid rounded" :src="recipe.image" style="max-height: 30vh;"
|
||||
:alt="$t( 'Recipe_Image')" v-if="recipe.image !== null">
|
||||
<div class="col-12 order-1 col-sm-12 order-sm-1 col-md-4 order-md-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img class="img img-fluid rounded" :src="recipe.image" style="max-height: 30vh" :alt="$t('Recipe_Image')" v-if="recipe.image !== null" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh; margin-bottom: 2vh">
|
||||
<div class="col-12">
|
||||
<Nutrition :recipe="recipe" :ingredient_factor="ingredient_factor"></Nutrition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh; margin-bottom: 2vh">
|
||||
<div class="col-12">
|
||||
<Nutrition-component :recipe="recipe" :ingredient_factor="ingredient_factor"></Nutrition-component>
|
||||
<template v-if="!recipe.internal">
|
||||
<div v-if="recipe.file_path.includes('.pdf')">
|
||||
<PdfViewer :recipe="recipe"></PdfViewer>
|
||||
</div>
|
||||
<div v-if="recipe.file_path.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
|
||||
<ImageViewer :recipe="recipe"></ImageViewer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-for="(s, index) in recipe.steps" v-bind:key="s.id" style="margin-top: 1vh">
|
||||
<Step
|
||||
:recipe="recipe"
|
||||
:step="s"
|
||||
:ingredient_factor="ingredient_factor"
|
||||
:index="index"
|
||||
:start_time="start_time"
|
||||
@update-start-time="updateStartTime"
|
||||
@checked-state-changed="updateIngredientCheckedState"
|
||||
></Step>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
|
||||
|
||||
</div>
|
||||
|
||||
<template v-if="!recipe.internal">
|
||||
<div v-if="recipe.file_path.includes('.pdf')">
|
||||
<PdfViewer :recipe="recipe"></PdfViewer>
|
||||
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh" v-if="share_uid !== 'None'">
|
||||
<div class="col col-md-12">
|
||||
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)">{{ $t("Report Abuse") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="recipe.file_path.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
|
||||
<ImageViewer :recipe="recipe"></ImageViewer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<div v-for="(s, index) in recipe.steps" v-bind:key="s.id" style="margin-top: 1vh">
|
||||
<step-component :recipe="recipe" :step="s" :ingredient_factor="ingredient_factor" :index="index" :start_time="start_time"
|
||||
@update-start-time="updateStartTime" @checked-state-changed="updateIngredientCheckedState"></step-component>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
|
||||
|
||||
<div class="row text-center d-print-none" style="margin-top: 3vh; margin-bottom: 3vh" v-if="share_uid !== 'None'">
|
||||
<div class="col col-md-12">
|
||||
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)">{{ $t('Report Abuse') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import {apiLoadRecipe} from "@/utils/api";
|
||||
import { apiLoadRecipe } from "@/utils/api"
|
||||
|
||||
import Step from "@/components/StepComponent";
|
||||
import RecipeContextMenu from "@/components/RecipeContextMenu";
|
||||
import {ResolveUrlMixin, ToastMixin} from "@/utils/utils";
|
||||
import Ingredient from "@/components/IngredientComponent";
|
||||
import Step from "@/components/Step"
|
||||
import RecipeContextMenu from "@/components/ContextMenu/RecipeContextMenu"
|
||||
import { ResolveUrlMixin, ToastMixin } from "@/utils/utils"
|
||||
import IngredientsCard from "@/components/IngredientsCard"
|
||||
|
||||
import PdfViewer from "@/components/PdfViewer";
|
||||
import ImageViewer from "@/components/ImageViewer";
|
||||
import Nutrition from "@/components/NutritionComponent";
|
||||
import PdfViewer from "@/components/PdfViewer"
|
||||
import ImageViewer from "@/components/ImageViewer"
|
||||
import Nutrition from "@/components/Nutrition"
|
||||
|
||||
import moment from 'moment'
|
||||
import Keywords from "@/components/KeywordsComponent";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import AddRecipeToBook from "@/components/AddRecipeToBook";
|
||||
import RecipeRating from "@/components/RecipeRating";
|
||||
import LastCooked from "@/components/LastCooked";
|
||||
import IngredientComponent from "@/components/IngredientComponent";
|
||||
import StepComponent from "@/components/StepComponent";
|
||||
import KeywordsComponent from "@/components/KeywordsComponent";
|
||||
import NutritionComponent from "@/components/NutritionComponent";
|
||||
import moment from "moment"
|
||||
import Keywords from "@/components/Keywords"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import AddRecipeToBook from "@/components/Modals/AddRecipeToBook"
|
||||
import RecipeRating from "@/components/RecipeRating"
|
||||
import LastCooked from "@/components/LastCooked"
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: 'RecipeView',
|
||||
mixins: [
|
||||
ResolveUrlMixin,
|
||||
ToastMixin,
|
||||
],
|
||||
components: {
|
||||
LastCooked,
|
||||
RecipeRating,
|
||||
PdfViewer,
|
||||
ImageViewer,
|
||||
IngredientComponent,
|
||||
StepComponent,
|
||||
RecipeContextMenu,
|
||||
NutritionComponent,
|
||||
KeywordsComponent,
|
||||
LoadingSpinner,
|
||||
AddRecipeToBook,
|
||||
},
|
||||
computed: {
|
||||
ingredient_factor: function () {
|
||||
return this.servings / this.recipe.servings
|
||||
name: "RecipeView",
|
||||
mixins: [ResolveUrlMixin, ToastMixin],
|
||||
components: {
|
||||
LastCooked,
|
||||
RecipeRating,
|
||||
PdfViewer,
|
||||
ImageViewer,
|
||||
IngredientsCard,
|
||||
Step,
|
||||
RecipeContextMenu,
|
||||
Nutrition,
|
||||
Keywords,
|
||||
LoadingSpinner,
|
||||
AddRecipeToBook,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
recipe: undefined,
|
||||
ingredient_count: 0,
|
||||
servings: 1,
|
||||
start_time: "",
|
||||
share_uid: window.SHARE_UID
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadRecipe(window.RECIPE_ID)
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
},
|
||||
methods: {
|
||||
loadRecipe: function (recipe_id) {
|
||||
apiLoadRecipe(recipe_id).then(recipe => {
|
||||
|
||||
if (window.USER_SERVINGS !== 0) {
|
||||
recipe.servings = window.USER_SERVINGS
|
||||
}
|
||||
this.servings = recipe.servings
|
||||
|
||||
let total_time = 0
|
||||
for (let step of recipe.steps) {
|
||||
this.ingredient_count += step.ingredients.length
|
||||
|
||||
for (let ingredient of step.ingredients) {
|
||||
this.$set(ingredient, 'checked', false)
|
||||
}
|
||||
|
||||
step.time_offset = total_time
|
||||
total_time += step.time
|
||||
}
|
||||
|
||||
// set start time only if there are any steps with timers (otherwise no timers are rendered)
|
||||
if (total_time > 0) {
|
||||
this.start_time = moment().format('yyyy-MM-DDTHH:mm')
|
||||
}
|
||||
|
||||
this.recipe = recipe
|
||||
this.loading = false
|
||||
})
|
||||
computed: {
|
||||
ingredient_factor: function () {
|
||||
return this.servings / this.recipe.servings
|
||||
},
|
||||
},
|
||||
updateStartTime: function (e) {
|
||||
this.start_time = e
|
||||
},
|
||||
updateIngredientCheckedState: function (e) {
|
||||
for (let step of this.recipe.steps) {
|
||||
for (let ingredient of step.ingredients) {
|
||||
if (ingredient.id === e.id) {
|
||||
this.$set(ingredient, 'checked', !ingredient.checked)
|
||||
}
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
recipe: undefined,
|
||||
ingredient_count: 0,
|
||||
servings: 1,
|
||||
start_time: "",
|
||||
share_uid: window.SHARE_UID,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
mounted() {
|
||||
this.loadRecipe(window.RECIPE_ID)
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
},
|
||||
methods: {
|
||||
loadRecipe: function (recipe_id) {
|
||||
apiLoadRecipe(recipe_id).then((recipe) => {
|
||||
if (window.USER_SERVINGS !== 0) {
|
||||
recipe.servings = window.USER_SERVINGS
|
||||
}
|
||||
this.servings = recipe.servings
|
||||
|
||||
let total_time = 0
|
||||
for (let step of recipe.steps) {
|
||||
this.ingredient_count += step.ingredients.length
|
||||
|
||||
for (let ingredient of step.ingredients) {
|
||||
this.$set(ingredient, "checked", false)
|
||||
}
|
||||
|
||||
step.time_offset = total_time
|
||||
total_time += step.time
|
||||
}
|
||||
|
||||
// set start time only if there are any steps with timers (otherwise no timers are rendered)
|
||||
if (total_time > 0) {
|
||||
this.start_time = moment().format("yyyy-MM-DDTHH:mm")
|
||||
}
|
||||
|
||||
this.recipe = recipe
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
updateStartTime: function (e) {
|
||||
this.start_time = e
|
||||
},
|
||||
|
||||
updateIngredientCheckedState: function (e) {
|
||||
for (let step of this.recipe.steps) {
|
||||
for (let ingredient of step.ingredients) {
|
||||
if (ingredient.id === e.id) {
|
||||
this.$set(ingredient, "checked", !ingredient.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
805
vue/src/apps/ShoppingListView/ShoppingListView.vue
Normal file
805
vue/src/apps/ShoppingListView/ShoppingListView.vue
Normal file
@@ -0,0 +1,805 @@
|
||||
<template>
|
||||
<div id="app" style="margin-bottom: 4vh">
|
||||
<b-alert :show="!online" dismissible class="small float-up" variant="warning">{{ $t("OfflineAlert") }}</b-alert>
|
||||
<div class="row float-top">
|
||||
<div class="offset-md-10 col-md-2 no-gutter text-right">
|
||||
<b-button variant="link" class="px-0">
|
||||
<i class="btn fas fa-plus-circle fa-lg px-0" @click="entrymode = !entrymode" :class="entrymode ? 'text-success' : 'text-muted'" />
|
||||
</b-button>
|
||||
<b-button variant="link" id="id_filters_button" class="px-0">
|
||||
<i class="btn fas fa-filter text-decoration-none fa-lg px-0" :class="filterApplied ? 'text-danger' : 'text-muted'" />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<!-- shopping list tab -->
|
||||
<b-tab :title="$t('ShoppingList')" active>
|
||||
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("ShoppingList") }} </template>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div role="tablist" v-if="items && items.length > 0">
|
||||
<div class="row justify-content-md-center w-75" v-if="entrymode">
|
||||
<div class="col col-md-2 "><b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input></div>
|
||||
<div class="col col-md-3">
|
||||
<generic-multiselect
|
||||
@change="new_item.unit = $event.val"
|
||||
:model="Models.UNIT"
|
||||
:multiple="false"
|
||||
:allow_create="false"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="$t('Unit')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col col-md-4">
|
||||
<generic-multiselect
|
||||
@change="new_item.food = $event.val"
|
||||
:model="Models.FOOD"
|
||||
:multiple="false"
|
||||
:allow_create="false"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="$t('Food')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col col-md-1 ">
|
||||
<b-button variant="link" class="px-0">
|
||||
<i class="btn fas fa-cart-plus fa-lg px-0 text-success" @click="addItem" />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(done, x) in Sections" :key="x">
|
||||
<div v-if="x == 'true'">
|
||||
<hr />
|
||||
<hr />
|
||||
<h4>{{ $t("Completed") }}</h4>
|
||||
</div>
|
||||
|
||||
<div v-for="(s, i) in done" :key="i">
|
||||
<h5 v-if="Object.entries(s).length > 0">
|
||||
<b-button
|
||||
class="btn btn-lg text-decoration-none text-dark px-1 py-0 border-0"
|
||||
variant="link"
|
||||
data-toggle="collapse"
|
||||
:href="'#section-' + sectionID(x, i)"
|
||||
:aria-expanded="'true' ? x == 'false' : 'true'"
|
||||
>
|
||||
<i class="fa fa-chevron-right rotate" />
|
||||
{{ i }}
|
||||
</b-button>
|
||||
</h5>
|
||||
|
||||
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
||||
<!-- passing an array of values to the table grouped by Food -->
|
||||
<div v-for="(entries, x) in Object.entries(s)" :key="x">
|
||||
<ShoppingLineItem :entries="entries[1]" :groupby="group_by" @open-context-menu="openContextMenu" @update-checkbox="updateChecked" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-tab>
|
||||
<!-- recipe tab -->
|
||||
<b-tab :title="$t('Recipes')">
|
||||
<table class="table w-75">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t("Meal_Plan") }}</th>
|
||||
<th scope="col">{{ $t("Recipe") }}</th>
|
||||
<th scope="col">{{ $t("Servings") }}</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="r in Recipes" :key="r.list_recipe">
|
||||
<td>{{ r.recipe_mealplan.name }}</td>
|
||||
<td>{{ r.recipe_mealplan.recipe_name }}</td>
|
||||
<td class="block-inline">
|
||||
<b-form-input min="1" type="number" :debounce="300" :value="r.recipe_mealplan.servings" @input="updateServings($event, r.list_recipe)"></b-form-input>
|
||||
</td>
|
||||
<td>
|
||||
<i class="btn text-danger fas fa-trash fa-lg px-2 border-0" variant="link" :title="$t('Delete')" @click="deleteRecipe($event, r.list_recipe)" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</b-tab>
|
||||
<!-- settings tab -->
|
||||
<b-tab :title="$t('Settings')">
|
||||
<div class="row">
|
||||
<div class="col col-md-4 ">
|
||||
<b-card class="no-body">
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="checkbox" size="sm" v-model="settings.mealplan_autoadd_shopping" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">{{ $t("mealplan_autoadd_shopping_desc") }}</em>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="settings.mealplan_autoadd_shopping">
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="checkbox" size="sm" v-model="settings.mealplan_autoexclude_onhand" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">{{ $t("mealplan_autoadd_shopping_desc") }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="settings.mealplan_autoadd_shopping">
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("mealplan_autoinclude_related") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="checkbox" size="sm" v-model="settings.mealplan_autoinclude_related" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">
|
||||
{{ $t("mealplan_autoinclude_related_desc") }}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("shopping_share") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<generic-multiselect
|
||||
size="sm"
|
||||
@change="
|
||||
settings.shopping_share = $event.val
|
||||
saveSettings()
|
||||
"
|
||||
:model="Models.USER"
|
||||
:initial_selection="settings.shopping_share"
|
||||
label="username"
|
||||
:multiple="true"
|
||||
:allow_create="false"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="$t('User')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">{{ $t("shopping_share_desc") }}</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("shopping_auto_sync") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="number" size="sm" v-model="settings.shopping_auto_sync" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">
|
||||
{{ $t("shopping_auto_sync_desc") }}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="checkbox" size="sm" v-model="settings.filter_to_supermarket" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">
|
||||
{{ $t("filter_to_supermarket_desc") }}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("default_delay") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="number" size="sm" min="1" v-model="settings.default_delay" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">
|
||||
{{ $t("default_delay_desc") }}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</b-card>
|
||||
</div>
|
||||
<div class="col col-md-8">
|
||||
<b-card class=" no-body">
|
||||
put the supermarket stuff here<br />
|
||||
-add supermarkets<br />
|
||||
-add supermarket categories<br />
|
||||
-sort supermarket categories<br />
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
|
||||
<div>
|
||||
<b-form-group v-bind:label="$t('GroupBy')" label-for="popover-input-1" label-cols="6" class="mb-1">
|
||||
<b-form-select v-model="group_by" :options="group_by_choices" size="sm"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1">
|
||||
<b-form-select v-model="selected_supermarket" :options="supermarkets" text-field="name" value-field="id" size="sm"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1" class="mb-1">
|
||||
<b-form-checkbox v-model="show_delay"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('ShowUncategorizedFood')" label-for="popover-input-4" content-cols="1" class="mb-1" v-if="!selected_supermarket">
|
||||
<b-form-checkbox v-model="show_undefined_categories"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('SupermarketCategoriesOnly')" label-for="popover-input-5" content-cols="1" class="mb-1" v-if="selected_supermarket">
|
||||
<b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div class="row " style="margin-top: 1vh;min-width:300px">
|
||||
<div class="col-12 " style="text-align: right;">
|
||||
<b-button size="sm" variant="primary" class="mx-1" @click="resetFilters">{{ $t("Reset") }} </b-button>
|
||||
<b-button size="sm" variant="secondary" class="mr-3" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-popover>
|
||||
<ContextMenu ref="menu">
|
||||
<template #menu="{ contextData }">
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
moveEntry($event, contextData)
|
||||
$refs.menu.close()
|
||||
"
|
||||
>
|
||||
<b-form-group label-cols="6" content-cols="6" class="text-nowrap m-0 mr-2">
|
||||
<template #label>
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-cubes"></i> {{ $t("MoveCategory") }}</a>
|
||||
</template>
|
||||
<span @click.prevent.stop @mouseup.prevent.stop>
|
||||
<!-- would like to hide the dropdown value and only display value in button - not sure how to do that -->
|
||||
<b-form-select class="mt-2 border-0" :options="shopping_categories" text-field="name" value-field="id" v-model="shopcat"></b-form-select>
|
||||
</span>
|
||||
</b-form-group>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
$refs.menu.close()
|
||||
onHand(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-clipboard-check"></i> {{ $t("OnHand") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
$refs.menu.close()
|
||||
delayThis(contextData)
|
||||
"
|
||||
>
|
||||
<b-form-group label-cols="10" content-cols="2" class="text-nowrap m-0 mr-2">
|
||||
<template #label>
|
||||
<a class="dropdown-item p-2" href="#"><i class="far fa-hourglass"></i> {{ $t("DelayFor", { hours: delay }) }}</a>
|
||||
</template>
|
||||
<div @click.prevent.stop>
|
||||
<b-form-input class="mt-2" min="0" type="number" v-model="delay"></b-form-input>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
$refs.menu.close()
|
||||
ignoreThis(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-ban"></i> {{ $t("IgnoreThis", { food: foodName(contextData) }) }}</a>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
$refs.menu.close()
|
||||
deleteThis(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2 text-danger" href="#"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
||||
import ShoppingLineItem from "@/components/ShoppingLineItem"
|
||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||
|
||||
import { ApiMixin, getUserPreference } from "@/utils/utils"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import { StandardToasts, makeToast } from "@/utils/utils"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: "ShoppingListView",
|
||||
mixins: [ApiMixin],
|
||||
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect },
|
||||
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
items: [],
|
||||
group_by: "category",
|
||||
group_by_choices: ["created_by", "category", "recipe"],
|
||||
supermarkets: [],
|
||||
shopping_categories: [],
|
||||
selected_supermarket: undefined,
|
||||
show_undefined_categories: true,
|
||||
supermarket_categories_only: false,
|
||||
shopcat: null,
|
||||
delay: 0,
|
||||
settings: {
|
||||
shopping_auto_sync: 0,
|
||||
default_delay: 4,
|
||||
mealplan_autoadd_shopping: false,
|
||||
mealplan_autoinclude_related: false,
|
||||
mealplan_autoexclude_onhand: true,
|
||||
filter_to_supermarket: false,
|
||||
},
|
||||
|
||||
autosync_id: undefined,
|
||||
auto_sync_running: false,
|
||||
show_delay: false,
|
||||
show_modal: false,
|
||||
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
|
||||
loading: true,
|
||||
entrymode: false,
|
||||
new_item: { amount: 1, unit: undefined, food: undefined },
|
||||
online: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Sections() {
|
||||
function getKey(item, group_by, x) {
|
||||
switch (group_by) {
|
||||
case "category":
|
||||
return item?.food?.supermarket_category?.name ?? x
|
||||
case "created_by":
|
||||
return item?.created_by?.username ?? x
|
||||
case "recipe":
|
||||
return item?.recipe_mealplan?.recipe_name ?? x
|
||||
}
|
||||
}
|
||||
|
||||
let shopping_list = this.items
|
||||
|
||||
// filter out list items that are delayed
|
||||
if (!this.show_delay && shopping_list) {
|
||||
shopping_list = shopping_list.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now()))
|
||||
}
|
||||
|
||||
// if a supermarket is selected and filtered to only supermarket categories filter out everything else
|
||||
if (this.selected_supermarket && this.supermarket_categories_only) {
|
||||
let shopping_categories = this.supermarkets // category IDs configured on supermarket
|
||||
.map((x) => x.category_to_supermarket)
|
||||
.flat()
|
||||
.map((x) => x.category.id)
|
||||
shopping_list = shopping_list.filter((x) => shopping_categories.includes(x?.food?.supermarket_category?.id))
|
||||
// if showing undefined is off, filter undefined
|
||||
} else if (!this.show_undefined_categories) {
|
||||
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
|
||||
}
|
||||
|
||||
let groups = { false: {}, true: {} } // force unchecked to always be first
|
||||
if (this.selected_supermarket) {
|
||||
let super_cats = this.supermarkets
|
||||
.filter((x) => x.id === this.selected_supermarket)
|
||||
.map((x) => x.category_to_supermarket)
|
||||
.flat()
|
||||
.map((x) => x.category.name)
|
||||
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).forEach((cat) => {
|
||||
groups["false"][cat.name] = {}
|
||||
groups["true"][cat.name] = {}
|
||||
})
|
||||
} else {
|
||||
this.shopping_categories.forEach((cat) => {
|
||||
groups.false[cat.name] = {}
|
||||
groups.true[cat.name] = {}
|
||||
})
|
||||
}
|
||||
|
||||
shopping_list.forEach((item) => {
|
||||
let key = getKey(item, this.group_by, this.$t("Undefined"))
|
||||
// first level of dict is done/not done
|
||||
if (!groups[item.checked]) groups[item.checked] = {}
|
||||
|
||||
// second level of dict is this.group_by selection
|
||||
if (!groups[item.checked][key]) groups[item.checked][key] = {}
|
||||
|
||||
// third level of dict is the food
|
||||
if (groups[item.checked][key][item.food.name]) {
|
||||
groups[item.checked][key][item.food.name].push(item)
|
||||
} else {
|
||||
groups[item.checked][key][item.food.name] = [item]
|
||||
}
|
||||
})
|
||||
return groups
|
||||
},
|
||||
defaultDelay() {
|
||||
return getUserPreference("default_delay") || 2
|
||||
},
|
||||
itemsDelayed() {
|
||||
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
|
||||
},
|
||||
filterApplied() {
|
||||
return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || (this.supermarket_categories_only && this.selected_supermarket)
|
||||
},
|
||||
Recipes() {
|
||||
return [...new Map(this.items.filter((x) => x.list_recipe).map((item) => [item["list_recipe"], item])).values()]
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selected_supermarket(newVal, oldVal) {
|
||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||
},
|
||||
"settings.filter_to_supermarket": function(newVal, oldVal) {
|
||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||
},
|
||||
"settings.shopping_auto_sync": function(newVal, oldVal) {
|
||||
clearInterval(this.autosync_id)
|
||||
this.autosync_id = undefined
|
||||
if (!newVal) {
|
||||
window.removeEventListener("online", this.updateOnlineStatus)
|
||||
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||
return
|
||||
} else if (oldVal === 0 && newVal > 0) {
|
||||
window.addEventListener("online", this.updateOnlineStatus)
|
||||
window.addEventListener("offline", this.updateOnlineStatus)
|
||||
}
|
||||
this.autosync_id = setInterval(() => {
|
||||
if (this.online && !this.auto_sync_running) {
|
||||
this.auto_sync_running = true
|
||||
this.getShoppingList(true)
|
||||
}
|
||||
}, this.settings.shopping_auto_sync * 1000)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getShoppingList()
|
||||
this.getSupermarkets()
|
||||
this.getShoppingCategories()
|
||||
|
||||
this.settings = getUserPreference()
|
||||
this.delay = this.settings.default_delay || 4
|
||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||
if (this.settings.shopping_auto_sync) {
|
||||
window.addEventListener("online", this.updateOnlineStatus)
|
||||
window.addEventListener("offline", this.updateOnlineStatus)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
addItem() {
|
||||
let api = new ApiApiFactory()
|
||||
api.createShoppingListEntry(this.new_item)
|
||||
.then((results) => {
|
||||
if (results?.data) {
|
||||
this.items.push(results.data)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
} else {
|
||||
console.log("no data returned")
|
||||
}
|
||||
this.new_item = { amount: 1 }
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
},
|
||||
categoryName: function(value) {
|
||||
return this.shopping_categories.filter((x) => x.id == value)[0]?.name ?? ""
|
||||
},
|
||||
resetFilters: function() {
|
||||
this.selected_supermarket = undefined
|
||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||
this.show_undefined_categories = true
|
||||
this.group_by = "category"
|
||||
this.show_delay = false
|
||||
},
|
||||
delayThis: function(item) {
|
||||
let entries = []
|
||||
let promises = []
|
||||
if (Array.isArray(item)) {
|
||||
entries = item.map((x) => x.id)
|
||||
} else {
|
||||
entries = [item.id]
|
||||
}
|
||||
let delay_date = new Date(Date.now() + this.delay * (60 * 60 * 1000))
|
||||
|
||||
entries.forEach((entry) => {
|
||||
promises.push(this.saveThis({ id: entry, delay_until: delay_date }, false))
|
||||
})
|
||||
Promise.all(promises).then(() => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
this.delay = this.defaultDelay
|
||||
})
|
||||
},
|
||||
deleteRecipe: function(e, recipe) {
|
||||
let api = new ApiApiFactory()
|
||||
api.destroyShoppingListRecipe(recipe)
|
||||
.then((x) => {
|
||||
this.items = this.items.filter((x) => x.list_recipe !== recipe)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
})
|
||||
},
|
||||
deleteThis: function(item) {
|
||||
let api = new ApiApiFactory()
|
||||
let entries = []
|
||||
let promises = []
|
||||
if (Array.isArray(item)) {
|
||||
entries = item.map((x) => x.id)
|
||||
} else {
|
||||
entries = [item.id]
|
||||
}
|
||||
|
||||
entries.forEach((x) => {
|
||||
promises.push(
|
||||
api.destroyShoppingListEntry(x).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
Promise.all(promises).then((result) => {
|
||||
this.items = this.items.filter((x) => !entries.includes(x.id))
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
})
|
||||
},
|
||||
foodName: function(value) {
|
||||
return value?.food?.name ?? value?.[0]?.food?.name ?? ""
|
||||
},
|
||||
getShoppingCategories: function() {
|
||||
let api = new ApiApiFactory()
|
||||
api.listSupermarketCategorys().then((result) => {
|
||||
this.shopping_categories = result.data
|
||||
})
|
||||
},
|
||||
getShoppingList: function(autosync = false) {
|
||||
let params = {}
|
||||
params.supermarket = this.selected_supermarket
|
||||
|
||||
params.options = { query: { recent: 1 } }
|
||||
if (autosync) {
|
||||
params.options.query["autosync"] = 1
|
||||
} else {
|
||||
this.loading = true
|
||||
}
|
||||
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
|
||||
.then((results) => {
|
||||
if (!autosync) {
|
||||
if (results.data?.length) {
|
||||
this.items = results.data
|
||||
} else {
|
||||
console.log("no data returned")
|
||||
}
|
||||
this.loading = false
|
||||
} else {
|
||||
this.mergeShoppingList(results.data)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
if (!autosync) {
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
}
|
||||
})
|
||||
},
|
||||
getSupermarkets: function() {
|
||||
let api = new ApiApiFactory()
|
||||
api.listSupermarkets().then((result) => {
|
||||
this.supermarkets = result.data
|
||||
})
|
||||
},
|
||||
getThis: function(id) {
|
||||
return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id })
|
||||
},
|
||||
ignoreThis: function(item) {
|
||||
let food = {
|
||||
id: item?.[0]?.food.id ?? item.food.id,
|
||||
ignore_shopping: true,
|
||||
}
|
||||
this.updateFood(food, "ignore_shopping")
|
||||
},
|
||||
mergeShoppingList: function(data) {
|
||||
this.items.map((x) =>
|
||||
data.map((y) => {
|
||||
if (y.id === x.id) {
|
||||
x.checked = y.checked
|
||||
return x
|
||||
}
|
||||
})
|
||||
)
|
||||
this.auto_sync_running = false
|
||||
},
|
||||
moveEntry: function(e, item) {
|
||||
if (!e) {
|
||||
makeToast(this.$t("Warning"), this.$t("NoCategory"), "warning")
|
||||
}
|
||||
|
||||
// TODO make decision - should inheritance always be turned off when category set manually or give user a choice at front-end or make it a setting?
|
||||
let food = this.items.filter((x) => x.food.id == item?.[0]?.food.id ?? item.food.id)[0].food
|
||||
food.supermarket_category = this.shopping_categories.filter((x) => x?.id === this.shopcat)?.[0]
|
||||
this.updateFood(food, "supermarket_category")
|
||||
this.shopcat = null
|
||||
},
|
||||
onHand: function(item) {
|
||||
let api = new ApiApiFactory()
|
||||
let food = {
|
||||
id: item?.[0]?.food.id ?? item?.food?.id,
|
||||
on_hand: true,
|
||||
}
|
||||
|
||||
this.updateFood(food)
|
||||
.then((result) => {
|
||||
let entries = this.items.filter((x) => x.food.id == food.id).map((x) => x.id)
|
||||
this.items = this.items.filter((x) => x.food.id !== food.id)
|
||||
return entries
|
||||
})
|
||||
.then((entries) => {
|
||||
entries.forEach((x) => {
|
||||
api.destroyShoppingListEntry(x).then((result) => {})
|
||||
})
|
||||
})
|
||||
},
|
||||
openContextMenu(e, value) {
|
||||
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
|
||||
this.$refs.menu.open(e, value)
|
||||
},
|
||||
saveSettings: function() {
|
||||
let api = ApiApiFactory()
|
||||
api.partialUpdateUserPreference(this.settings.user, this.settings)
|
||||
.then((result) => {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
},
|
||||
saveThis: function(thisItem, toast = true) {
|
||||
let api = new ApiApiFactory()
|
||||
if (!thisItem?.id) {
|
||||
// if there is no item id assume it's a new item
|
||||
return api
|
||||
.createShoppingListEntry(thisItem)
|
||||
.then((result) => {
|
||||
if (toast) {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
} else {
|
||||
return api
|
||||
.partialUpdateShoppingListEntry(thisItem.id, thisItem)
|
||||
.then((result) => {
|
||||
if (toast) {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
}
|
||||
},
|
||||
sectionID: function(a, b) {
|
||||
return (a + b).replace(/\W/g, "")
|
||||
},
|
||||
updateChecked: function(update) {
|
||||
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
|
||||
let promises = []
|
||||
update.entries.forEach((x) => {
|
||||
promises.push(this.saveThis({ id: x, checked: update.checked }, false))
|
||||
let item = this.items.filter((entry) => entry.id == x)[0]
|
||||
|
||||
Vue.set(item, "checked", update.checked)
|
||||
if (update.checked) {
|
||||
Vue.set(item, "completed_at", new Date().toISOString())
|
||||
} else {
|
||||
Vue.set(item, "completed_at", undefined)
|
||||
}
|
||||
})
|
||||
Promise.all(promises).catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
},
|
||||
updateFood: function(food, field) {
|
||||
let api = new ApiApiFactory()
|
||||
let ignore_category
|
||||
if (field) {
|
||||
ignore_category = food.ignore_inherit
|
||||
.map((x) => food.ignore_inherit.fields)
|
||||
.flat()
|
||||
.includes(field)
|
||||
} else {
|
||||
ignore_category = true
|
||||
}
|
||||
|
||||
return api
|
||||
.partialUpdateFood(food.id, food)
|
||||
.then((result) => {
|
||||
if (food.inherit && food.supermarket_category && !ignore_category && food.parent) {
|
||||
makeToast(this.$t("Warning"), this.$t("InheritWarning", { food: food.name }), "warning")
|
||||
} else {
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
}
|
||||
if (food?.numchild > 0) {
|
||||
this.getShoppingList() // if food has children, just get the whole list. probably could be more efficient
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, Object.keys(err))
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
},
|
||||
updateServings(e, plan) {
|
||||
// maybe this needs debounced?
|
||||
let api = new ApiApiFactory()
|
||||
api.partialUpdateShoppingListRecipe(plan, { id: plan, servings: e }).then(() => {
|
||||
this.getShoppingList()
|
||||
})
|
||||
},
|
||||
updateOnlineStatus(e) {
|
||||
const { type } = e
|
||||
this.online = type === "online"
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("online", this.updateOnlineStatus)
|
||||
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--style src="vue-multiselect/dist/vue-multiselect.min.css"></style-->
|
||||
|
||||
<style>
|
||||
.rotate {
|
||||
-moz-transition: all 0.25s linear;
|
||||
-webkit-transition: all 0.25s linear;
|
||||
transition: all 0.25s linear;
|
||||
}
|
||||
.btn[aria-expanded="true"] > .rotate {
|
||||
-moz-transform: rotate(90deg);
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.float-top {
|
||||
padding-bottom: -3em;
|
||||
margin-bottom: -3em;
|
||||
}
|
||||
.float-up {
|
||||
padding-top: -3em;
|
||||
margin-top: -3em;
|
||||
}
|
||||
</style>
|
||||
10
vue/src/apps/ShoppingListView/main.js
Normal file
10
vue/src/apps/ShoppingListView/main.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import i18n from "@/i18n"
|
||||
import Vue from "vue"
|
||||
import App from "./ShoppingListView"
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app")
|
||||
Reference in New Issue
Block a user