Fix after rebase

This commit is contained in:
smilerz
2021-10-28 07:35:30 -05:00
parent f16e457d14
commit 10a33add75
73 changed files with 5579 additions and 2524 deletions

View File

@@ -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>

View File

@@ -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')

View File

@@ -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

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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>

View 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>

View 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")