mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-03 13:19:16 -05:00
Merge branch 'feature/export-progress' into develop
This commit is contained in:
@@ -54,14 +54,20 @@
|
||||
<div class="col-12 col-md-3 calender-options">
|
||||
<h5>{{ $t("Planner_Settings") }}</h5>
|
||||
<b-form>
|
||||
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')" label-for="UomInput">
|
||||
<b-form-select id="UomInput" v-model="settings.displayPeriodUom" :options="options.displayPeriodUom"></b-form-select>
|
||||
<b-form-group id="UomInput" :label="$t('Period')" :description="$t('Plan_Period_To_Show')"
|
||||
label-for="UomInput">
|
||||
<b-form-select id="UomInput" v-model="settings.displayPeriodUom"
|
||||
:options="options.displayPeriodUom"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group id="PeriodInput" :label="$t('Periods')" :description="$t('Plan_Show_How_Many_Periods')" label-for="PeriodInput">
|
||||
<b-form-select id="PeriodInput" v-model="settings.displayPeriodCount" :options="options.displayPeriodCount"></b-form-select>
|
||||
<b-form-group id="PeriodInput" :label="$t('Periods')"
|
||||
:description="$t('Plan_Show_How_Many_Periods')" label-for="PeriodInput">
|
||||
<b-form-select id="PeriodInput" v-model="settings.displayPeriodCount"
|
||||
:options="options.displayPeriodCount"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')" label-for="DaysInput">
|
||||
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek" :options="dayNames"></b-form-select>
|
||||
<b-form-group id="DaysInput" :label="$t('Starting_Day')" :description="$t('Starting_Day')"
|
||||
label-for="DaysInput">
|
||||
<b-form-select id="DaysInput" v-model="settings.startingDayOfWeek"
|
||||
:options="dayNames"></b-form-select>
|
||||
</b-form-group>
|
||||
<b-form-group id="WeekNumInput" :label="$t('Week_Numbers')">
|
||||
<b-form-checkbox v-model="settings.displayWeekNumbers" name="week_num">
|
||||
@@ -73,19 +79,24 @@
|
||||
<div class="col-12 col-md-9 col-lg-6">
|
||||
<h5>{{ $t("Meal_Types") }}</h5>
|
||||
<div>
|
||||
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10" handle=".handle" @sort="sortMealTypes()">
|
||||
<b-card no-body class="mt-1" v-for="(meal_type, index) in meal_types" v-hover :key="meal_type.id">
|
||||
<b-card-header class="p-4">
|
||||
<draggable :list="meal_types" group="meal_types" :empty-insert-threshold="10" @sort="sortMealTypes()" ghost-class="ghost">
|
||||
<b-card no-body class="mt-1 list-group-item p-2" style="cursor:move" v-for="(meal_type, index) in meal_types" v-hover
|
||||
:key="meal_type.id">
|
||||
<b-card-header class="p-2 border-0">
|
||||
<div class="row">
|
||||
<div class="col-2 handle">
|
||||
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
|
||||
<div class="col-2">
|
||||
<button type="button" class="btn btn-lg shadow-none"><i
|
||||
class="fas fa-arrows-alt-v"></i></button>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<h5>
|
||||
{{ meal_type.icon }} {{ meal_type.name
|
||||
}}<span class="float-right text-primary"
|
||||
><i class="fa" v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }" @click="editOrSaveMealType(index)" aria-hidden="true"></i
|
||||
></span>
|
||||
<h5 class="mt-1 mb-1">
|
||||
{{ meal_type.icon }} {{
|
||||
meal_type.name
|
||||
}}<span class="float-right text-primary" style="cursor:pointer"
|
||||
><i class="fa"
|
||||
v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }"
|
||||
@click="editOrSaveMealType(index)" aria-hidden="true"></i
|
||||
></span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,20 +104,29 @@
|
||||
<b-card-body class="p-4" v-if="meal_type.editing">
|
||||
<div class="form-group">
|
||||
<label>{{ $t("Name") }}</label>
|
||||
<input class="form-control" placeholder="Name" v-model="meal_type.name" />
|
||||
<input class="form-control" placeholder="Name" v-model="meal_type.name"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<emoji-input :field="'icon'" :label="$t('Icon')" :value="meal_type.icon"></emoji-input>
|
||||
<emoji-input :field="'icon'" :label="$t('Icon')"
|
||||
:value="meal_type.icon"></emoji-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t("Color") }}</label>
|
||||
<input class="form-control" type="color" name="Name" :value="meal_type.color" @change="meal_type.color = $event.target.value" />
|
||||
<input class="form-control" type="color" name="Name"
|
||||
:value="meal_type.color"
|
||||
@change="meal_type.color = $event.target.value"/>
|
||||
</div>
|
||||
<b-form-checkbox id="checkbox-1" v-model="meal_type.default" name="default_checkbox" class="mb-2">
|
||||
<b-form-checkbox id="checkbox-1" v-model="meal_type.default"
|
||||
name="default_checkbox" class="mb-2">
|
||||
{{ $t("Default") }}
|
||||
</b-form-checkbox>
|
||||
<button class="btn btn-danger" @click="deleteMealType(index)">{{ $t("Delete") }}</button>
|
||||
<button class="btn btn-primary float-right" @click="editOrSaveMealType(index)">{{ $t("Save") }}</button>
|
||||
<button class="btn btn-danger" @click="deleteMealType(index)">{{
|
||||
$t("Delete")
|
||||
}}
|
||||
</button>
|
||||
<button class="btn btn-primary float-right" @click="editOrSaveMealType(index)">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</draggable>
|
||||
@@ -127,7 +147,15 @@
|
||||
openEntryEdit(contextData.originalItem.entry)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{ $t("Edit") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{
|
||||
$t("Edit")
|
||||
}}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="contextData.originalItem.entry.recipe != null"
|
||||
@click="$refs.menu.close();openRecipe(contextData.originalItem.entry.recipe)">
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pizza-slice"></i>
|
||||
{{ $t("Recipe") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@@ -135,7 +163,8 @@
|
||||
moveEntryLeft(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i> {{ $t("Move") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i>
|
||||
{{ $t("Move") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@@ -143,7 +172,8 @@
|
||||
moveEntryRight(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i> {{ $t("Move") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i>
|
||||
{{ $t("Move") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@@ -159,7 +189,8 @@
|
||||
addToShopping(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i> {{ $t("Add_to_Shopping") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i>
|
||||
{{ $t("Add_to_Shopping") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@@ -167,7 +198,8 @@
|
||||
deleteEntry(contextData)
|
||||
"
|
||||
>
|
||||
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
||||
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i>
|
||||
{{ $t("Delete") }}</a>
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
@@ -198,10 +230,12 @@
|
||||
<div class="col-12 mt-1" v-if="shopping_list.length > 0">
|
||||
<b-button-group>
|
||||
<b-button variant="success" @click="saveShoppingList"
|
||||
><i class="fas fa-external-link-alt"></i>
|
||||
><i class="fas fa-external-link-alt"></i>
|
||||
{{ $t("Open") }}
|
||||
</b-button>
|
||||
<b-button variant="danger" @click="shopping_list = []"><i class="fa fa-trash"></i> {{ $t("Clear") }} </b-button>
|
||||
<b-button variant="danger" @click="shopping_list = []"><i class="fa fa-trash"></i>
|
||||
{{ $t("Clear") }}
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,37 +243,46 @@
|
||||
</div>
|
||||
</template>
|
||||
<transition name="slide-fade">
|
||||
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)" v-if="current_tab === 0">
|
||||
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background: rgba(255, 255, 255, 0.6)"
|
||||
v-if="current_tab === 0">
|
||||
<div class="col-md-3 col-6">
|
||||
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i class="fas fa-calendar-plus"></i> {{ $t("Create") }}</button>
|
||||
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i
|
||||
class="fas fa-calendar-plus"></i> {{ $t("Create") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<button class="btn btn-block btn-primary shadow-none" v-b-toggle.sidebar-shopping><i class="fas fa-shopping-cart"></i> {{ $t("Shopping_list") }}</button>
|
||||
<button class="btn btn-block btn-primary shadow-none" v-b-toggle.sidebar-shopping><i
|
||||
class="fas fa-shopping-cart"></i> {{ $t("Shopping_list") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"
|
||||
><i class="fas fa-download"></i>
|
||||
><i class="fas fa-download"></i>
|
||||
{{ $t("Export_To_ICal") }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top :title="$t('Coming_Soon')">
|
||||
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top
|
||||
:title="$t('Coming_Soon')">
|
||||
{{ $t("Auto_Planner") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center mt-2 d-block d-md-none">
|
||||
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
|
||||
<b-button-group class="mx-1">
|
||||
<b-button v-html="'<<'" @click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
|
||||
<b-button v-html="'<<'"
|
||||
@click="setShowDate($refs.header.headerProps.previousPeriod)"></b-button>
|
||||
<b-button v-html="'<'" @click="setStartingDay(-1)"></b-button>
|
||||
</b-button-group>
|
||||
<b-button-group class="mx-1">
|
||||
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i class="fas fa-home"></i> </b-button>
|
||||
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i
|
||||
class="fas fa-home"></i></b-button>
|
||||
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
|
||||
</b-button-group>
|
||||
<b-button-group class="mx-1">
|
||||
<b-button v-html="'>'" @click="setStartingDay(1)"></b-button>
|
||||
<b-button v-html="'>>'" @click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
|
||||
<b-button v-html="'>>'"
|
||||
@click="setShowDate($refs.header.headerProps.nextPeriod)"></b-button>
|
||||
</b-button-group>
|
||||
</b-button-toolbar>
|
||||
</div>
|
||||
@@ -250,7 +293,7 @@
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||
@@ -264,11 +307,11 @@ import moment from "moment"
|
||||
import draggable from "vuedraggable"
|
||||
import VueCookies from "vue-cookies"
|
||||
|
||||
import { ApiMixin, StandardToasts } from "@/utils/utils"
|
||||
import { CalendarView, CalendarMathMixin } from "vue-simple-calendar/src/components/bundle"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import {ApiMixin, StandardToasts, ResolveUrlMixin} from "@/utils/utils"
|
||||
import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle"
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
|
||||
const { makeToast } = require("@/utils/utils")
|
||||
const {makeToast} = require("@/utils/utils")
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
Vue.use(BootstrapVue)
|
||||
@@ -288,7 +331,7 @@ export default {
|
||||
EmojiInput,
|
||||
draggable,
|
||||
},
|
||||
mixins: [CalendarMathMixin, ApiMixin],
|
||||
mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin],
|
||||
data: function () {
|
||||
return {
|
||||
showDate: new Date(),
|
||||
@@ -306,12 +349,12 @@ export default {
|
||||
current_context_menu_item: null,
|
||||
options: {
|
||||
displayPeriodUom: [
|
||||
{ text: this.$t("Week"), value: "week" },
|
||||
{text: this.$t("Week"), value: "week"},
|
||||
{
|
||||
text: this.$t("Month"),
|
||||
value: "month",
|
||||
},
|
||||
{ text: this.$t("Year"), value: "year" },
|
||||
{text: this.$t("Year"), value: "year"},
|
||||
],
|
||||
displayPeriodCount: [1, 2, 3],
|
||||
entryEditing: {
|
||||
@@ -369,7 +412,7 @@ export default {
|
||||
dayNames: function () {
|
||||
let options = []
|
||||
this.getFormattedWeekdayNames(this.userLocale, "long", 0).forEach((day, index) => {
|
||||
options.push({ text: day, value: index })
|
||||
options.push({text: day, value: index})
|
||||
})
|
||||
return options
|
||||
},
|
||||
@@ -411,6 +454,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openRecipe: function (recipe) {
|
||||
window.open(this.resolveDjangoUrl('view_recipe', recipe.id))
|
||||
},
|
||||
addToShopping(entry) {
|
||||
if (entry.originalItem.entry.recipe !== null) {
|
||||
this.shopping_list.push(entry.originalItem.entry)
|
||||
@@ -445,7 +491,7 @@ export default {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient
|
||||
.createMealType({ name: this.$t("Meal_Type") })
|
||||
.createMealType({name: this.$t("Meal_Type")})
|
||||
.then((e) => {
|
||||
this.periodChangedCallback(this.current_period)
|
||||
})
|
||||
@@ -831,4 +877,9 @@ having to override as much.
|
||||
.theme-default .cv-day.draghover {
|
||||
box-shadow: inset 0 0 0.2em 0.2em yellow;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<h3>
|
||||
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
|
||||
<model-menu />
|
||||
<span>{{ this.this_model.name }}</span>
|
||||
<span>{{ $t(this.this_model.name) }}</span>
|
||||
<span v-if="apiName !== 'Step'">
|
||||
<b-button variant="link" @click="startAction({ action: 'new' })">
|
||||
<i class="fas fa-plus-circle fa-2x"></i>
|
||||
@@ -242,26 +242,6 @@ export default {
|
||||
let results = result.data?.results ?? result.data
|
||||
|
||||
if (results?.length) {
|
||||
// let secondaryRequest = undefined;
|
||||
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
|
||||
// // the item list is smaller than it should be based on the site the user is own
|
||||
// // this happens when an item is deleted (or merged)
|
||||
// // to prevent issues insert the last item of the previous search page before loading the new results
|
||||
// params.page = params.page - 1
|
||||
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
||||
// let prev_page_results = result.data?.results ?? result.data
|
||||
// if (prev_page_results?.length) {
|
||||
// results = [prev_page_results[prev_page_results.length]].concat(results)
|
||||
//
|
||||
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
|
||||
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
// this[column + '_counts']['max'] = result.data?.count ?? 0
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
|
||||
this["items_" + column] = this["items_" + column].concat(results)
|
||||
this[column + "_counts"]["current"] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
this[column + "_counts"]["max"] = result.data?.count ?? 0
|
||||
@@ -280,11 +260,32 @@ export default {
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, { id: id })
|
||||
},
|
||||
saveThis: function (item) {
|
||||
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||
// then place all new items at the top of the list - could sort instead
|
||||
this.items_left = [item].concat(this.destroyCard(item?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
this.items_right = [{ ...item }].concat(this.destroyCard(item?.id, this.items_right))
|
||||
if (!item?.id) {
|
||||
// if there is no item id assume it's a new item
|
||||
this.genericAPI(this.this_model, this.Actions.CREATE, item)
|
||||
.then((result) => {
|
||||
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||
// then place all new items at the top of the list - could sort instead
|
||||
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
this.items_right = [{ ...result.data }].concat(this.destroyCard(result?.data?.id, this.items_right))
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
} else {
|
||||
this.genericAPI(this.this_model, this.Actions.UPDATE, item)
|
||||
.then((result) => {
|
||||
this.refreshThis(item.id)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
}
|
||||
},
|
||||
// this currently assumes shopping is only applicable on FOOD model
|
||||
addShopping: function (food) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="app" style="margin-bottom: 4vh">
|
||||
<RecipeSwitcher mode="mealplan" />
|
||||
<RecipeSwitcher ref="ref_recipe_switcher" />
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
|
||||
<div class="row">
|
||||
@@ -8,25 +8,25 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
|
||||
<b-input-group>
|
||||
<b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="settings.search_input" v-bind:placeholder="$t('Search')"></b-input>
|
||||
<b-input class="form-control form-control-lg form-control-borderless form-control-search" v-model="search.search_input" v-bind:placeholder="$t('Search')"></b-input>
|
||||
<b-input-group-append>
|
||||
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()" v-if="debug">
|
||||
<b-button v-b-tooltip.hover :title="$t('show_sql')" @click="showSQL()" v-if="debug && ui.sql_debug">
|
||||
<i class="fas fa-bug" style="font-size: 1.5em"></i>
|
||||
</b-button>
|
||||
<b-button variant="light" v-b-tooltip.hover :title="$t('Random Recipes')" @click="openRandom()">
|
||||
<i class="fas fa-dice-five" style="font-size: 1.5em"></i>
|
||||
</b-button>
|
||||
<b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover :title="$t('Advanced Settings')" v-bind:variant="!isAdvancedSettingsSet() ? 'primary' : 'danger'">
|
||||
<b-button v-b-toggle.collapse_advanced_search v-b-tooltip.hover :title="$t('Advanced Settings')" v-bind:variant="!searchFiltered(true) ? 'primary' : 'danger'">
|
||||
<!-- TODO consider changing this icon to a filter -->
|
||||
<i class="fas fa-caret-down" v-if="!settings.advanced_search_visible"></i>
|
||||
<i class="fas fa-caret-up" v-if="settings.advanced_search_visible"></i>
|
||||
<i class="fas fa-caret-down" v-if="!search.advanced_search_visible"></i>
|
||||
<i class="fas fa-caret-up" v-if="search.advanced_search_visible"></i>
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="settings.advanced_search_visible">
|
||||
<b-collapse id="collapse_advanced_search" class="mt-2 shadow-sm" v-model="search.advanced_search_visible">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<div class="row">
|
||||
@@ -41,12 +41,13 @@
|
||||
class="btn btn-block text-uppercase"
|
||||
v-b-tooltip.hover
|
||||
:title="$t('show_only_internal')"
|
||||
v-bind:class="{ 'btn-success': settings.search_internal, 'btn-primary': !settings.search_internal }"
|
||||
v-bind:class="{ 'btn-success': search.search_internal, 'btn-primary': !search.search_internal }"
|
||||
@click="
|
||||
settings.search_internal = !settings.search_internal
|
||||
search.search_internal = !search.search_internal
|
||||
refreshData()
|
||||
"
|
||||
>
|
||||
<!-- TODO is this the right place to refresh data? or deep watch on this.search?? -->
|
||||
{{ $t("Internal") }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -56,33 +57,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-popover target="id_settings_button" triggers="click" placement="bottom" :title="$t('Settings')">
|
||||
<div>
|
||||
<b-form-group v-bind:label="$t('Recently_Viewed')" label-for="popover-input-1" label-cols="6" class="mb-3">
|
||||
<b-form-input type="number" v-model="settings.recently_viewed" id="popover-input-1" size="sm"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-popover target="id_settings_button" triggers="click" placement="bottom">
|
||||
<b-tabs content-class="mt-1" small>
|
||||
<b-tab :title="$t('Settings')" active>
|
||||
<b-form-group v-bind:label="$t('Recently_Viewed')" label-for="popover-input-1" label-cols="6" class="mb-3">
|
||||
<b-form-input type="number" v-model="ui.recently_viewed" id="popover-input-1" size="sm"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-bind:label="$t('Recipes_per_page')" label-for="popover-input-page-count" label-cols="6" class="mb-3">
|
||||
<b-form-input type="number" v-model="settings.page_count" id="popover-input-page-count" size="sm"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('Recipes_per_page')" label-for="popover-input-page-count" label-cols="6" class="mb-3">
|
||||
<b-form-input type="number" v-model="ui.page_size" id="popover-input-page-count" size="sm"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2" label-cols="6" class="mb-3">
|
||||
<b-form-checkbox switch v-model="settings.show_meal_plan" id="popover-input-2" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('Meal_Plan')" label-for="popover-input-2" label-cols="6" class="mb-3">
|
||||
<b-form-checkbox switch v-model="ui.show_meal_plan" id="popover-input-2" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-if="settings.show_meal_plan" v-bind:label="$t('Meal_Plan_Days')" label-for="popover-input-5" label-cols="6" class="mb-3">
|
||||
<b-form-input type="number" v-model="settings.meal_plan_days" id="popover-input-5" size="sm"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="ui.show_meal_plan" v-bind:label="$t('Meal_Plan_Days')" label-for="popover-input-5" label-cols="6" class="mb-3">
|
||||
<b-form-input type="number" v-model="ui.meal_plan_days" id="popover-input-5" size="sm"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group v-bind:label="$t('Sort_by_new')" label-for="popover-input-3" label-cols="6" class="mb-3">
|
||||
<b-form-checkbox switch v-model="ui.sort_by_new" id="popover-input-3" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
<div class="col-12">
|
||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t("Search Settings") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab title="Expert Settings">
|
||||
<b-form-group v-bind:label="$t('remember_search')" label-for="popover-rem-search" label-cols="6" class="mb-3">
|
||||
<b-form-checkbox switch v-model="ui.remember_search" id="popover-rem-search" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="ui.remember_search" v-bind:label="$t('remember_hours')" label-for="popover-input-rem-hours" label-cols="6" class="mb-3">
|
||||
<b-form-input type="number" v-model="ui.remember_hours" id="popover-rem-hours" size="sm"></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group v-bind:label="$t('tree_select')" label-for="popover-input-treeselect" label-cols="6" class="mb-3">
|
||||
<b-form-checkbox switch v-model="ui.tree_select" id="popover-input-treeselect" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="debug" v-bind:label="$t('sql_debug')" label-for="popover-input-sqldebug" label-cols="6" class="mb-3">
|
||||
<b-form-checkbox switch v-model="ui.sql_debug" id="popover-input-sqldebug" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
<b-form-group v-bind:label="$t('Sort_by_new')" label-for="popover-input-3" label-cols="6" class="mb-3">
|
||||
<b-form-checkbox switch v-model="settings.sort_by_new" id="popover-input-3" size="sm"></b-form-checkbox>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
<div class="col-12">
|
||||
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t("Advanced Search Settings") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1vh">
|
||||
<div class="col-12" style="text-align: right">
|
||||
<b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
|
||||
@@ -95,20 +113,33 @@
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2">
|
||||
<treeselect
|
||||
v-model="settings.search_keywords"
|
||||
v-if="ui.tree_select"
|
||||
v-model="search.search_keywords"
|
||||
:options="facets.Keywords"
|
||||
:load-options="loadKeywordChildren"
|
||||
:multiple="true"
|
||||
:flat="true"
|
||||
:auto-load-root-options="false"
|
||||
searchNested
|
||||
multiple
|
||||
:placeholder="$t('Keywords')"
|
||||
:normalizer="normalizer"
|
||||
@input="refreshData(false)"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
/>
|
||||
<generic-multiselect
|
||||
v-if="!ui.tree_select"
|
||||
@change="genericSelectChanged"
|
||||
parent_variable="search_keywords"
|
||||
:initial_selection="search.search_keywords"
|
||||
:model="Models.KEYWORD"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="$t('Keywords')"
|
||||
:limit="50"
|
||||
></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="settings.search_keywords_or" name="check-button" @change="refreshData(false)" class="shadow-none" switch>
|
||||
<span class="text-uppercase" v-if="settings.search_keywords_or">{{ $t("or") }}</span>
|
||||
<b-form-checkbox v-model="search.search_keywords_or" name="check-button" @change="refreshData(false)" class="shadow-none" switch>
|
||||
<span class="text-uppercase" v-if="search.search_keywords_or">{{ $t("or") }}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@@ -122,20 +153,33 @@
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2">
|
||||
<treeselect
|
||||
v-model="settings.search_foods"
|
||||
v-if="ui.tree_select"
|
||||
v-model="search.search_foods"
|
||||
:options="facets.Foods"
|
||||
:load-options="loadFoodChildren"
|
||||
:multiple="true"
|
||||
:flat="true"
|
||||
:auto-load-root-options="false"
|
||||
searchNested
|
||||
multiple
|
||||
:placeholder="$t('Ingredients')"
|
||||
:normalizer="normalizer"
|
||||
@input="refreshData(false)"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
/>
|
||||
<generic-multiselect
|
||||
v-if="!ui.tree_select"
|
||||
@change="genericSelectChanged"
|
||||
parent_variable="search_foods"
|
||||
:initial_selection="search.search_foods"
|
||||
:model="Models.FOOD"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="$t('Foods')"
|
||||
:limit="50"
|
||||
></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="settings.search_foods_or" name="check-button" @change="refreshData(false)" class="shadow-none" switch>
|
||||
<span class="text-uppercase" v-if="settings.search_foods_or">{{ $t("or") }}</span>
|
||||
<b-form-checkbox v-model="search.search_foods_or" name="check-button" @change="refreshData(false)" class="shadow-none" switch>
|
||||
<span class="text-uppercase" v-if="search.search_foods_or">{{ $t("or") }}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@@ -151,7 +195,7 @@
|
||||
<generic-multiselect
|
||||
@change="genericSelectChanged"
|
||||
parent_variable="search_books"
|
||||
:initial_selection="settings.search_books"
|
||||
:initial_selection="search.search_books"
|
||||
:model="Models.RECIPE_BOOK"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Books')"
|
||||
@@ -159,8 +203,8 @@
|
||||
></generic-multiselect>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text>
|
||||
<b-form-checkbox v-model="settings.search_books_or" name="check-button" @change="refreshData(false)" class="shadow-none" tyle="width: 100%" switch>
|
||||
<span class="text-uppercase" v-if="settings.search_books_or">{{ $t("or") }}</span>
|
||||
<b-form-checkbox v-model="search.search_books_or" name="check-button" @change="refreshData(false)" class="shadow-none" tyle="width: 100%" switch>
|
||||
<span class="text-uppercase" v-if="search.search_books_or">{{ $t("or") }}</span>
|
||||
<span class="text-uppercase" v-else>{{ $t("and") }}</span>
|
||||
</b-form-checkbox>
|
||||
</b-input-group-text>
|
||||
@@ -174,7 +218,7 @@
|
||||
<div class="col-12">
|
||||
<b-input-group class="mt-2">
|
||||
<treeselect
|
||||
v-model="settings.search_ratings"
|
||||
v-model="search.search_ratings"
|
||||
:options="ratingOptions"
|
||||
:flat="true"
|
||||
:placeholder="$t('Ratings')"
|
||||
@@ -183,7 +227,7 @@
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-input-group-text style="width: 85px"> </b-input-group-text>
|
||||
<b-input-group-text style="width: 85px"></b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
@@ -197,7 +241,7 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-right" style="margin-top: 2vh">
|
||||
<span class="text-muted">
|
||||
{{ $t("Page") }} {{ settings.pagination_page }}/{{ Math.ceil(pagination_count / settings.page_count) }}
|
||||
{{ $t("Page") }} {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }}
|
||||
<a href="#" @click="resetSearch"><i class="fas fa-times-circle"></i> {{ $t("Reset") }}</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -206,17 +250,17 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-gap: 0.8rem">
|
||||
<template v-if="!searchFiltered">
|
||||
<template v-if="!searchFiltered()">
|
||||
<recipe-card v-bind:key="`mp_${m.id}`" v-for="m in meal_plans" :recipe="m.recipe" :meal_plan="m" :footer_text="m.meal_type_name" footer_icon="far fa-calendar-alt"></recipe-card>
|
||||
</template>
|
||||
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" :footer_text="isRecentOrNew(r)[0]" :footer_icon="isRecentOrNew(r)[1]"> </recipe-card>
|
||||
<recipe-card v-for="r in recipes" v-bind:key="r.id" :recipe="r" :footer_text="isRecentOrNew(r)[0]" :footer_icon="isRecentOrNew(r)[1]"></recipe-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh" v-if="!random_search">
|
||||
<div class="col col-md-12">
|
||||
<b-pagination pills v-model="settings.pagination_page" :total-rows="pagination_count" :per-page="settings.page_count" @change="pageChange" align="center"> </b-pagination>
|
||||
<b-pagination pills v-model="search.pagination_page" :total-rows="pagination_count" :per-page="ui.page_size" @change="pageChange" align="center"></b-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,28 +272,25 @@
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import VueCookies from "vue-cookies"
|
||||
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import moment from "moment"
|
||||
import _debounce from "lodash/debounce"
|
||||
|
||||
import VueCookies from "vue-cookies"
|
||||
|
||||
Vue.use(VueCookies)
|
||||
|
||||
import { ApiMixin, ResolveUrlMixin } from "@/utils/utils"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprecated?
|
||||
|
||||
import RecipeCard from "@/components/RecipeCard"
|
||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||
import Treeselect from "@riophae/vue-treeselect"
|
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||
import { Treeselect, LOAD_CHILDREN_OPTIONS } from "@riophae/vue-treeselect" //TODO: delete
|
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css" //TODO: delete
|
||||
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
|
||||
|
||||
Vue.use(VueCookies)
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
let SETTINGS_COOKIE_NAME = "search_settings"
|
||||
let SEARCH_COOKIE_NAME = "search_settings"
|
||||
let UI_COOKIE_NAME = "_uisearch_settings"
|
||||
|
||||
export default {
|
||||
name: "RecipeSearchView",
|
||||
@@ -263,27 +304,30 @@ export default {
|
||||
meal_plans: [],
|
||||
last_viewed_recipes: [],
|
||||
|
||||
settings_loaded: false,
|
||||
settings: {
|
||||
search: {
|
||||
advanced_search_visible: false,
|
||||
search_input: "",
|
||||
search_internal: false,
|
||||
search_keywords: [],
|
||||
search_foods: [],
|
||||
search_books: [],
|
||||
search_ratings: undefined,
|
||||
|
||||
search_keywords_or: true,
|
||||
search_foods_or: true,
|
||||
search_books_or: true,
|
||||
advanced_search_visible: false,
|
||||
pagination_page: 1,
|
||||
},
|
||||
ui: {
|
||||
show_meal_plan: true,
|
||||
meal_plan_days: 0,
|
||||
recently_viewed: 5,
|
||||
sort_by_new: true,
|
||||
pagination_page: 1,
|
||||
page_count: 25,
|
||||
page_size: 25,
|
||||
remember_search: true,
|
||||
remember_hours: 4,
|
||||
sql_debug: false,
|
||||
tree_select: false,
|
||||
},
|
||||
|
||||
pagination_count: 0,
|
||||
random_search: false,
|
||||
debug: false,
|
||||
@@ -291,58 +335,75 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
ratingOptions: function () {
|
||||
return [
|
||||
{ id: 5, label: "⭐⭐⭐⭐⭐" + " (" + (this.facets.Ratings?.["5.0"] ?? 0) + ")" },
|
||||
{ id: 4, label: "⭐⭐⭐⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["4.0"] ?? 0) + ")" },
|
||||
{ id: 3, label: "⭐⭐⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["3.0"] ?? 0) + ")" },
|
||||
{ id: 2, label: "⭐⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["2.0"] ?? 0) + ")" },
|
||||
{ id: 1, label: "⭐ " + this.$t("and_up") + " (" + (this.facets.Ratings?.["1.0"] ?? 0) + ")" },
|
||||
{ id: -1, label: this.$t("Unrated") + " (" + (this.facets.Ratings?.["0.0"] ?? 0) + ")" },
|
||||
]
|
||||
},
|
||||
searchFiltered: function () {
|
||||
if (
|
||||
this.settings?.search_input === "" &&
|
||||
this.settings?.search_keywords?.length === 0 &&
|
||||
this.settings?.search_foods?.length === 0 &&
|
||||
this.settings?.search_books?.length === 0 &&
|
||||
// this.settings?.pagination_page === 1 &&
|
||||
!this.random_search &&
|
||||
this.settings?.search_ratings === undefined
|
||||
) {
|
||||
return false
|
||||
let ratingCount = undefined
|
||||
if (Object.keys(this.facets?.Ratings ?? {}).length === 0) {
|
||||
ratingCount = (x) => {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
ratingCount = (x) => {
|
||||
return ` (${x})`
|
||||
}
|
||||
}
|
||||
return [
|
||||
{ id: 5, label: "⭐⭐⭐⭐⭐" + ratingCount(this.facets.Ratings?.["5.0"] ?? 0) },
|
||||
{ id: 4, label: "⭐⭐⭐⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["4.0"] ?? 0) },
|
||||
{ id: 3, label: "⭐⭐⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["3.0"] ?? 0) },
|
||||
{ id: 2, label: "⭐⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["2.0"] ?? 0) },
|
||||
{ id: 1, label: "⭐ " + this.$t("and_up") + ratingCount(this.facets.Ratings?.["1.0"] ?? 0) },
|
||||
{ id: 0, label: this.$t("Unrated") + ratingCount(this.facets.Ratings?.["0.0"] ?? 0) },
|
||||
]
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(function () {
|
||||
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
||||
this.settings = Object.assign({}, this.settings, this.$cookies.get(SETTINGS_COOKIE_NAME))
|
||||
if (this.$cookies.isKey(UI_COOKIE_NAME)) {
|
||||
this.ui = Object.assign({}, this.ui, this.$cookies.get(UI_COOKIE_NAME))
|
||||
}
|
||||
if (this.ui.remember_search && this.$cookies.isKey(SEARCH_COOKIE_NAME)) {
|
||||
this.search = Object.assign({}, this.search, this.$cookies.get(SEARCH_COOKIE_NAME), `${this.ui.remember_hours}h`)
|
||||
}
|
||||
let urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
if (urlParams.has("keyword")) {
|
||||
this.settings.search_keywords = []
|
||||
this.search.search_keywords = []
|
||||
this.facets.Keywords = []
|
||||
for (let x of urlParams.getAll("keyword")) {
|
||||
this.settings.search_keywords.push(Number.parseInt(x))
|
||||
this.facets.Keywords.push({ id: x, name: "loading..." })
|
||||
let initial_keyword = { id: Number.parseInt(x), name: "loading..." }
|
||||
this.search.search_keywords.push(initial_keyword)
|
||||
|
||||
this.genericAPI(this.Models.KEYWORD, this.Actions.FETCH, { id: initial_keyword.id })
|
||||
.then((response) => {
|
||||
let kw_index = this.search.search_keywords.findIndex((k) => k.id === initial_keyword.id)
|
||||
this.$set(this.search.search_keywords, kw_index, response.data)
|
||||
this.$set(this.facets.Keywords, kw_index, response.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response.status === 404) {
|
||||
let kw_index = this.search.search_keywords.findIndex((k) => k.id === initial_keyword.id)
|
||||
this.search.search_keywords.splice(kw_index, 1)
|
||||
this.facets.Keywords.splice(kw_index, 1)
|
||||
this.refreshData(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.facets.Foods = []
|
||||
for (let x of this.settings.search_foods) {
|
||||
for (let x of this.search.search_foods) {
|
||||
this.facets.Foods.push({ id: x, name: "loading..." })
|
||||
}
|
||||
|
||||
this.facets.Keywords = []
|
||||
for (let x of this.settings.search_keywords) {
|
||||
for (let x of this.search.search_keywords) {
|
||||
this.facets.Keywords.push({ id: x, name: "loading..." })
|
||||
}
|
||||
|
||||
this.facets.Books = []
|
||||
for (let x of this.settings.search_books) {
|
||||
for (let x of this.search.search_books) {
|
||||
this.facets.Books.push({ id: x, name: "loading..." })
|
||||
}
|
||||
|
||||
this.loadMealPlan()
|
||||
this.refreshData(false)
|
||||
})
|
||||
@@ -350,27 +411,38 @@ export default {
|
||||
this.debug = localStorage.getItem("DEBUG") == "True" || false
|
||||
},
|
||||
watch: {
|
||||
settings: {
|
||||
search: {
|
||||
handler() {
|
||||
this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, "4h")
|
||||
this.$cookies.set(SEARCH_COOKIE_NAME, this.search, `${this.ui.remember_hours}h`)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
"settings.show_meal_plan": function () {
|
||||
ui: {
|
||||
handler() {
|
||||
this.$cookies.set(UI_COOKIE_NAME, this.ui)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
"ui.show_meal_plan": function () {
|
||||
this.loadMealPlan()
|
||||
},
|
||||
"settings.meal_plan_days": function () {
|
||||
"ui.meal_plan_days": function () {
|
||||
this.loadMealPlan()
|
||||
},
|
||||
"settings.recently_viewed": function () {
|
||||
"ui.recently_viewed": function () {
|
||||
this.refreshData(false)
|
||||
},
|
||||
"settings.search_input": _debounce(function () {
|
||||
this.settings.pagination_page = 1
|
||||
"ui.tree_select": function () {
|
||||
if (this.ui.tree_select && !this.facets?.Keywords && !this.facets?.Foods) {
|
||||
this.getFacets(this.facets?.hash)
|
||||
}
|
||||
},
|
||||
"search.search_input": _debounce(function () {
|
||||
this.search.pagination_page = 1
|
||||
this.pagination_count = 0
|
||||
this.refreshData(false)
|
||||
}, 300),
|
||||
"settings.page_count": _debounce(function () {
|
||||
"ui.page_size": _debounce(function () {
|
||||
this.refreshData(false)
|
||||
}, 300),
|
||||
},
|
||||
@@ -378,42 +450,26 @@ export default {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
refreshData: function (random) {
|
||||
this.random_search = random
|
||||
let params = {
|
||||
query: this.settings.search_input,
|
||||
keywords: this.settings.search_keywords,
|
||||
foods: this.settings.search_foods,
|
||||
rating: this.settings.search_ratings,
|
||||
books: this.settings.search_books.map(function (A) {
|
||||
return A["id"]
|
||||
}),
|
||||
keywordsOr: this.settings.search_keywords_or,
|
||||
foodsOr: this.settings.search_foods_or,
|
||||
booksOr: this.settings.search_books_or,
|
||||
internal: this.settings.search_internal,
|
||||
random: this.random_search,
|
||||
_new: this.settings.sort_by_new,
|
||||
page: this.settings.pagination_page,
|
||||
pageSize: this.settings.page_count,
|
||||
}
|
||||
if (!this.searchFiltered) {
|
||||
params.options = { query: { last_viewed: this.settings.recently_viewed } }
|
||||
}
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
|
||||
window.scrollTo(0, 0)
|
||||
this.pagination_count = result.data.count
|
||||
let params = this.buildParams()
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
window.scrollTo(0, 0)
|
||||
this.pagination_count = result.data.count
|
||||
|
||||
this.facets = result.data.facets
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key)
|
||||
}
|
||||
this.recipes = this.removeDuplicates(result.data.results, (recipe) => recipe.id)
|
||||
if (!this.searchFiltered) {
|
||||
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
|
||||
let mealPlans = []
|
||||
this.meal_plans.forEach((x) => mealPlans.push(x.recipe.id))
|
||||
this.recipes = this.recipes.filter((recipe) => !mealPlans.includes(recipe.id))
|
||||
}
|
||||
})
|
||||
this.facets = result.data.facets
|
||||
this.recipes = this.removeDuplicates(result.data.results, (recipe) => recipe.id)
|
||||
if (!this.searchFiltered()) {
|
||||
// if meal plans are being shown - filter out any meal plan recipes from the recipe list
|
||||
let mealPlans = []
|
||||
this.meal_plans.forEach((x) => mealPlans.push(x.recipe.id))
|
||||
this.recipes = this.recipes.filter((recipe) => !mealPlans.includes(recipe.id))
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.$nextTick(function () {
|
||||
this.getFacets(this.facets?.cache_key)
|
||||
})
|
||||
})
|
||||
},
|
||||
openRandom: function () {
|
||||
this.refreshData(true)
|
||||
@@ -422,12 +478,12 @@ export default {
|
||||
return [...new Map(data.map((item) => [key(item), item])).values()]
|
||||
},
|
||||
loadMealPlan: function () {
|
||||
if (this.settings.show_meal_plan) {
|
||||
if (this.ui.show_meal_plan) {
|
||||
let params = {
|
||||
options: {
|
||||
query: {
|
||||
from_date: moment().format("YYYY-MM-DD"),
|
||||
to_date: moment().add(this.settings.meal_plan_days, "days").format("YYYY-MM-DD"),
|
||||
to_date: moment().add(this.ui.meal_plan_days, "days").format("YYYY-MM-DD"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -439,26 +495,24 @@ export default {
|
||||
}
|
||||
},
|
||||
genericSelectChanged: function (obj) {
|
||||
this.settings[obj.var] = obj.val
|
||||
this.search[obj.var] = obj.val
|
||||
this.refreshData(false)
|
||||
},
|
||||
resetSearch: function () {
|
||||
this.settings.search_input = ""
|
||||
this.settings.search_internal = false
|
||||
this.settings.search_keywords = []
|
||||
this.settings.search_foods = []
|
||||
this.settings.search_books = []
|
||||
this.settings.search_ratings = undefined
|
||||
this.settings.pagination_page = 1
|
||||
this.search.search_input = ""
|
||||
this.search.search_internal = false
|
||||
this.search.search_keywords = []
|
||||
this.search.search_foods = []
|
||||
this.search.search_books = []
|
||||
this.search.search_ratings = undefined
|
||||
this.search.pagination_page = 1
|
||||
this.refreshData(false)
|
||||
},
|
||||
pageChange: function (page) {
|
||||
this.settings.pagination_page = page
|
||||
this.search.pagination_page = page
|
||||
this.refreshData(false)
|
||||
},
|
||||
isAdvancedSettingsSet() {
|
||||
return this.settings.search_keywords.length + this.settings.search_foods.length + this.settings.search_books.length > 0
|
||||
},
|
||||
|
||||
normalizer(node) {
|
||||
let count = node?.count ? " (" + node.count + ")" : ""
|
||||
return {
|
||||
@@ -473,44 +527,82 @@ export default {
|
||||
let new_recipe = [this.$t("New_Recipe"), "fas fa-splotch"]
|
||||
if (x.new) {
|
||||
return new_recipe
|
||||
} else if (this.facets.Recent.includes(x.id)) {
|
||||
} else if (x.recent) {
|
||||
return recent_recipe
|
||||
} else {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
},
|
||||
getFacets: function (hash) {
|
||||
this.genericGetAPI("api_get_facets", { hash: hash }).then((response) => {
|
||||
getFacets: function (hash, facet, id) {
|
||||
if (!this.ui.tree_select) {
|
||||
return
|
||||
}
|
||||
let params = { hash: hash }
|
||||
if (facet) {
|
||||
params[facet] = id
|
||||
}
|
||||
return this.genericGetAPI("api_get_facets", params).then((response) => {
|
||||
this.facets = { ...this.facets, ...response.data.facets }
|
||||
})
|
||||
},
|
||||
showSQL: function () {
|
||||
// TODO refactor this so that it isn't a total copy of refreshData
|
||||
let params = this.buildParams()
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {})
|
||||
},
|
||||
// TODO refactor to combine with load KeywordChildren
|
||||
loadFoodChildren({ action, parentNode, callback }) {
|
||||
if (action === LOAD_CHILDREN_OPTIONS) {
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key, "food", parentNode.id).then(callback())
|
||||
}
|
||||
}
|
||||
},
|
||||
loadKeywordChildren({ action, parentNode, callback }) {
|
||||
if (action === LOAD_CHILDREN_OPTIONS) {
|
||||
if (this.facets?.cache_key) {
|
||||
this.getFacets(this.facets.cache_key, "keyword", parentNode.id).then(callback())
|
||||
}
|
||||
}
|
||||
},
|
||||
buildParams: function () {
|
||||
let params = {
|
||||
query: this.settings.search_input,
|
||||
keywords: this.settings.search_keywords,
|
||||
foods: this.settings.search_foods,
|
||||
rating: this.settings.search_ratings,
|
||||
books: this.settings.search_books.map(function (A) {
|
||||
query: this.search.search_input,
|
||||
keywords: this.search.search_keywords.map(function (A) {
|
||||
return A?.["id"] ?? A
|
||||
}),
|
||||
foods: this.search.search_foods.map(function (A) {
|
||||
return A?.["id"] ?? A
|
||||
}),
|
||||
rating: this.search.search_ratings,
|
||||
books: this.search.search_books.map(function (A) {
|
||||
return A["id"]
|
||||
}),
|
||||
keywordsOr: this.settings.search_keywords_or,
|
||||
foodsOr: this.settings.search_foods_or,
|
||||
booksOr: this.settings.search_books_or,
|
||||
internal: this.settings.search_internal,
|
||||
keywordsOr: this.search.search_keywords_or,
|
||||
foodsOr: this.search.search_foods_or,
|
||||
booksOr: this.search.search_books_or,
|
||||
internal: this.search.search_internal,
|
||||
random: this.random_search,
|
||||
_new: this.settings.sort_by_new,
|
||||
page: this.settings.pagination_page,
|
||||
pageSize: this.settings.page_count,
|
||||
_new: this.ui.sort_by_new,
|
||||
page: this.search.pagination_page,
|
||||
pageSize: this.ui.page_size,
|
||||
}
|
||||
if (!this.searchFiltered) {
|
||||
params.options = { query: { last_viewed: this.settings.recently_viewed, debug: true } }
|
||||
if (!this.searchFiltered()) {
|
||||
params.options = { query: { last_viewed: this.ui.recently_viewed } }
|
||||
}
|
||||
return params
|
||||
},
|
||||
searchFiltered: function (ignore_string = false) {
|
||||
let filtered =
|
||||
this.search?.search_keywords?.length === 0 &&
|
||||
this.search?.search_foods?.length === 0 &&
|
||||
this.search?.search_books?.length === 0 &&
|
||||
!this.random_search &&
|
||||
this.search?.search_ratings === undefined
|
||||
if (ignore_string) {
|
||||
return !filtered
|
||||
} else {
|
||||
params.options = { query: { debug: true } }
|
||||
return !filtered && this.search?.search_input !== ""
|
||||
}
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
|
||||
console.log(result.data)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<div v-if="!loading">
|
||||
<RecipeSwitcher :recipe="rootrecipe.id" :name="rootrecipe.name" mode="recipe" @switch="quickSwitch($event)" />
|
||||
<RecipeSwitcher ref="ref_recipe_switcher" @switch="quickSwitch($event)" />
|
||||
<div class="row">
|
||||
<div class="col-12" style="text-align: center">
|
||||
<h3>{{ recipe.name }}</h3>
|
||||
@@ -65,15 +65,7 @@
|
||||
<i class="fas fa-pizza-slice fa-2x text-primary"></i>
|
||||
</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"
|
||||
/>
|
||||
<CustomInputSpinButton v-model.number="servings" />
|
||||
</div>
|
||||
<div class="my-auto">
|
||||
<span class="text-primary">
|
||||
@@ -174,6 +166,7 @@ import StepComponent from "@/components/StepComponent"
|
||||
import KeywordsComponent from "@/components/KeywordsComponent"
|
||||
import NutritionComponent from "@/components/NutritionComponent"
|
||||
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
|
||||
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
|
||||
|
||||
Vue.prototype.moment = moment
|
||||
|
||||
@@ -195,6 +188,7 @@ export default {
|
||||
LoadingSpinner,
|
||||
AddRecipeToBook,
|
||||
RecipeSwitcher,
|
||||
CustomInputSpinButton,
|
||||
},
|
||||
computed: {
|
||||
ingredient_factor: function () {
|
||||
|
||||
@@ -1,102 +1,140 @@
|
||||
<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="row float-top pl-0 pr-0">
|
||||
<div class="col-auto no-gutter ml-auto">
|
||||
<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 variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
|
||||
<i class="btn fas fa-plus-circle fa-lg px-0" @click="entrymode = !entrymode" :class="entrymode ? 'text-success' : 'text-primary'" />
|
||||
</b-button>
|
||||
<b-button variant="link" class="px-1">
|
||||
<i class="fas fa-download fa-lg nav-link dropdown-toggle text-muted px-1" id="downloadShoppingLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></i>
|
||||
<b-button variant="link" class="px-1 pt-0 pb-1 d-none d-md-inline-block">
|
||||
<i class="fas fa-download fa-lg nav-link dropdown-toggle text-primary px-1" id="downloadShoppingLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></i>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-center" aria-labelledby="downloadShoppingLink">
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="downloadShoppingLink">
|
||||
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
||||
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')" icon="fas fa-file-csv" />
|
||||
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')" icon="fas fa-clipboard-list" />
|
||||
<CopyToClipboard :items="csvData" :settings="settings" format="table" :label="$t('copy_markdown_table')" icon="fab fa-markdown" />
|
||||
</div>
|
||||
</b-button>
|
||||
<b-button variant="link" id="id_filters_button" class="px-1">
|
||||
<i class="btn fas fa-filter text-decoration-none fa-lg px-1" :class="filterApplied ? 'text-danger' : 'text-muted'" />
|
||||
<b-button variant="link" id="id_filters_button" class="px-1 pt-0 pb-1">
|
||||
<i class="btn fas fa-filter text-decoration-none fa-lg px-1" :class="filterApplied ? 'text-danger' : 'text-primary'" />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-tabs content-class="mt-3">
|
||||
<b-tabs content-class="mt-3" v-model="current_tab">
|
||||
<!-- shopping list tab -->
|
||||
<b-tab active>
|
||||
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("Shopping_list") }} </template>
|
||||
<div class="container" id="shoppinglist">
|
||||
<template #title>
|
||||
<b-spinner v-if="loading" type="border" small></b-spinner>
|
||||
{{ $t("Shopping_list") }}
|
||||
</template>
|
||||
<div class="container p-0" id="shoppinglist">
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="col col-md-12 p-0 p-lg-3">
|
||||
<div role="tablist">
|
||||
<!-- add to shopping form -->
|
||||
|
||||
<b-row class="row justify-content-md-center" v-if="entrymode">
|
||||
<b-col cols="12" sm="4" md="2" v-if="!entry_mode_simple">
|
||||
<b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input>
|
||||
<b-row class="justify-content-md-center align-items-center pl-1 pr-1" v-if="entrymode">
|
||||
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-none d-md-block mt-1">
|
||||
<b-form-input
|
||||
size="lg"
|
||||
min="1"
|
||||
type="number"
|
||||
:description="$t('Amount')"
|
||||
v-model="new_item.amount"
|
||||
style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important"
|
||||
></b-form-input>
|
||||
</b-col>
|
||||
<b-col cols="12" sm="8" md="3" v-if="!entry_mode_simple">
|
||||
<lookup-input :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" :clear="clear" />
|
||||
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1">
|
||||
<lookup-input :class_list="'mb-0'" :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" :clear="clear" />
|
||||
</b-col>
|
||||
<b-col cols="12" sm="8" md="4" v-if="!entry_mode_simple">
|
||||
<lookup-input :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" :clear="clear" />
|
||||
<b-col cols="12" md="4" v-if="!entry_mode_simple" class="mt-1">
|
||||
<lookup-input :class_list="'mb-0'" :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" :clear="clear" />
|
||||
</b-col>
|
||||
<b-col cols="12" sm="8" v-if="entry_mode_simple">
|
||||
<b-form-input type="text" :placeholder="$t('QuickEntry')" v-model="new_item.ingredient" @keyup.enter="addItem"></b-form-input>
|
||||
<b-col cols="12" md="11" v-if="entry_mode_simple" class="mt-1">
|
||||
<b-form-input size="lg" type="text" :placeholder="$t('QuickEntry')" v-model="new_item.ingredient" @keyup.enter="addItem"></b-form-input>
|
||||
</b-col>
|
||||
<b-col cols="12" sm="4" md="1">
|
||||
<b-col cols="12" md="1" class="d-none d-md-block mt-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>
|
||||
</b-col>
|
||||
<b-col cols="12" md="3" v-if="!entry_mode_simple" class="d-block d-md-none mt-1">
|
||||
<b-row>
|
||||
<b-col cols="9">
|
||||
<b-form-input
|
||||
size="lg"
|
||||
min="1"
|
||||
type="number"
|
||||
:description="$t('Amount')"
|
||||
v-model="new_item.amount"
|
||||
style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important"
|
||||
></b-form-input>
|
||||
</b-col>
|
||||
<b-col cols="3" class="flex-grow-1">
|
||||
<b-button variant="success" class="p-0 pt-1 w-100 h-100">
|
||||
<i class="btn fas fa-cart-plus fa-lg" @click="addItem" />
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="row justify-content-md-end" v-if="entrymode">
|
||||
<b-row class="row justify-content-around mt-2" v-if="entrymode">
|
||||
<b-form-checkbox switch v-model="entry_mode_simple">
|
||||
{{ $t("QuickEntry") }}
|
||||
</b-form-checkbox>
|
||||
<b-button variant="success" size="sm" class="d-flex d-md-none p-0" v-if="entry_mode_simple">
|
||||
<i class="btn fas fa-cart-plus" @click="addItem" />
|
||||
</b-button>
|
||||
</b-row>
|
||||
<!-- shopping list table -->
|
||||
<div v-if="items && items.length > 0">
|
||||
<div v-for="(done, x) in Sections" :key="x">
|
||||
<div v-if="x == 'true'">
|
||||
<hr />
|
||||
<hr />
|
||||
<h4>{{ $t("Completed") }}</h4>
|
||||
<div v-if="x == 'true'" class="bg-header w-100 text-center d-flex justify-content-center align-items-center">
|
||||
<span class="h4 d-flex mt-1 mb-1">{{ $t("Completed") }}</span>
|
||||
</div>
|
||||
|
||||
<div v-for="(s, i) in done" :key="i">
|
||||
<h5 v-if="Object.entries(s).length > 0">
|
||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
type="button"
|
||||
class="btn dropdown-toggle btn-link text-decoration-none text-dark pr-2 dropdown-toggle-no-caret"
|
||||
@click.stop="openContextMenu($event, s, true)"
|
||||
>
|
||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||
</button>
|
||||
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" v-if="Object.entries(s).length > 0">
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
type="button"
|
||||
class="btn dropdown-toggle btn-link text-decoration-none text-dark pr-2 dropdown-toggle-no-caret"
|
||||
@click.stop="openContextMenu($event, s, true)"
|
||||
>
|
||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</h5>
|
||||
<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" />
|
||||
<span class="h5 ml-2 text-secondary">{{ i }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<transition-group name="slide-fade">
|
||||
<div v-for="(entries, x) in Object.entries(s)" :key="x">
|
||||
<transition name="slide-fade" mode="out-in">
|
||||
<ShoppingLineItem
|
||||
:entries="entries[1]"
|
||||
:groupby="group_by"
|
||||
:settings="settings"
|
||||
@open-context-menu="openContextMenu"
|
||||
@update-checkbox="updateChecked"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,33 +146,63 @@
|
||||
</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>
|
||||
<div class="container p-0">
|
||||
<b-row class="justify-content-md-center align-items-center p-1">
|
||||
<b-col cols="10">
|
||||
<b-input-group>
|
||||
<b-input-group-prepend is-text>
|
||||
{{ $t("Servings") }}
|
||||
</b-input-group-prepend>
|
||||
<b-input-group-prepend is-text>
|
||||
<input type="number" :min="1" v-model="add_recipe_servings" style="width: 3em" />
|
||||
</b-input-group-prepend>
|
||||
<!-- <b-input-group-prepend is-text>
|
||||
<b>{{ $t("Recipe") }}</b>
|
||||
</b-input-group-prepend> -->
|
||||
<generic-multiselect
|
||||
class="input-group-text m-0 p-0"
|
||||
@change="new_recipe = $event.val"
|
||||
:label="'name'"
|
||||
:model="Models.RECIPE"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Recipe')"
|
||||
:limit="20"
|
||||
:multiple="false"
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-button variant="success" @click="addRecipeToShopping" :disabled="!new_recipe.id">{{ $t("Add_to_Shopping") }}</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<table class="table w-100">
|
||||
<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>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
</b-tab>
|
||||
<!-- supermarkets tab -->
|
||||
<b-tab :title="$t('Supermarkets')">
|
||||
<div class="row justify-content-center">
|
||||
<!-- supermarkets column -->
|
||||
<div class="col col-md-5">
|
||||
<b-card>
|
||||
<b-card no-body>
|
||||
<template #header>
|
||||
<h4 class="mb-0">
|
||||
{{ $t("Supermarkets") }}
|
||||
@@ -151,81 +219,76 @@
|
||||
})
|
||||
"
|
||||
>
|
||||
<i class="btn fas fa-plus-circle fa-lg px-0" :class="new_supermarket.entrymode ? 'text-success' : 'text-muted'" />
|
||||
<i class="btn fas fa-plus-circle fa-lg px-0" :class="new_supermarket.entrymode ? 'text-success' : 'text-primary'" />
|
||||
</b-button>
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<b-card
|
||||
class="m-1 p-1 no-body"
|
||||
class="pt-5 pl-5 pr-5"
|
||||
border-variant="success"
|
||||
header-bg-variant="success"
|
||||
header-text-variant="white"
|
||||
align="center"
|
||||
v-if="new_supermarket.entrymode"
|
||||
:header="$t('SupermarketName')"
|
||||
:header="new_supermarket.value ? new_supermarket.value : $t('SupermarketName')"
|
||||
>
|
||||
<div class="input-group">
|
||||
<b-form-input type="text" :placeholder="$t('SupermarketName')" v-model="new_supermarket.value" />
|
||||
<b-button class="input-group-append" variant="success" @click="addSupermarket"><i class="pr-2 pt-1 fas fa-save"></i> {{ $t("Save") }}</b-button>
|
||||
</div>
|
||||
<b-input-group>
|
||||
<b-form-input type="text" class="form-control-append" :placeholder="$t('SupermarketName')" v-model="new_supermarket.value" />
|
||||
<b-input-group-append>
|
||||
<b-button class="input-group-append" variant="success" @click="addSupermarket"><i class="pr-2 pt-1 fas fa-save"></i> {{ $t("Create") }} </b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-card>
|
||||
|
||||
<b-card-body class="m-0 p-0">
|
||||
<b-card class="no-body mb-2" v-for="s in supermarkets" v-bind:key="s.id">
|
||||
<b-card-title>
|
||||
<b-card class="mt-1 p-0" v-for="s in supermarkets" v-bind:key="s.id">
|
||||
<b-card-header class="p-2 border-0 pt-3">
|
||||
<div class="row">
|
||||
<div class="col">{{ s.name }}</div>
|
||||
|
||||
<div class="col-auto text-right ml-auto">
|
||||
<b-button
|
||||
variant="link"
|
||||
class="p-0 m-0"
|
||||
@click="
|
||||
s.editmode = !s.editmode
|
||||
new_category.entrymode = false
|
||||
new_supermarket.entrymode = false
|
||||
editSupermarket(s)
|
||||
"
|
||||
>
|
||||
<i class="btn fas fa-edit fa-lg px-0" :class="s.editmode ? 'text-success' : 'text-muted'" />
|
||||
</b-button>
|
||||
<b-button variant="link" class="p-0 m-0" @click="deleteSupermarket(s)">
|
||||
<i class="btn fas fa-trash fa-lg px-2 text-muted" />
|
||||
</b-button>
|
||||
<div class="col-12">
|
||||
<h5 class="mt-1 mb-1">
|
||||
{{ s.name }}
|
||||
<b-button
|
||||
variant="link"
|
||||
class="p-0 m-0 float-right"
|
||||
@click="
|
||||
s.editmode = !s.editmode
|
||||
new_category.entrymode = false
|
||||
new_supermarket.entrymode = false
|
||||
editSupermarket(s)
|
||||
"
|
||||
>
|
||||
<i class="btn fas fa-edit fa-lg px-0" :class="s.editmode ? 'text-success' : 'text-primary'" />
|
||||
</b-button>
|
||||
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteSupermarket(s)">
|
||||
<i class="btn fas fa-trash fa-lg px-2 text-danger" />
|
||||
</b-button>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-title>
|
||||
|
||||
<b-card-body class="py-0">
|
||||
</b-card-header>
|
||||
<b-card-body class="m-0 p-0">
|
||||
<generic-pill :item_list="s.category_to_supermarket" label="category::name" color="info"></generic-pill>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
<!-- supermarket category column -->
|
||||
<div class="col col-md-5">
|
||||
<b-card class="no-body">
|
||||
<b-card>
|
||||
<template #header>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ $t("Shopping_Categories") }}
|
||||
</div>
|
||||
|
||||
<div class="col-auto text-right ml-auto">
|
||||
<b-button
|
||||
variant="link"
|
||||
class="p-0 m-0"
|
||||
@click="
|
||||
new_category.entrymode = !new_category.entrymode
|
||||
new_supermarket.entrymode = false
|
||||
"
|
||||
>
|
||||
<i class="btn fas fa-plus-circle fa-lg px-0" :class="new_category.entrymode ? 'text-success' : 'text-muted'" />
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="mb-0">
|
||||
{{ $t("Shopping_Categories") }}
|
||||
<b-button
|
||||
variant="link"
|
||||
class="p-0 m-0 float-right"
|
||||
@click="
|
||||
new_category.entrymode = !new_category.entrymode
|
||||
new_supermarket.entrymode = false
|
||||
"
|
||||
>
|
||||
<i class="btn fas fa-plus-circle fa-lg px-0" :class="new_category.entrymode ? 'text-success' : 'text-primary'" />
|
||||
</b-button>
|
||||
</h4>
|
||||
</template>
|
||||
<b-card
|
||||
class="m-1 p-1 no-body"
|
||||
@@ -234,15 +297,17 @@
|
||||
header-text-variant="white"
|
||||
align="center"
|
||||
v-if="new_category.entrymode"
|
||||
:header="$t('CategoryName')"
|
||||
:header="new_category.value ? new_category.value : $t('CategoryName')"
|
||||
>
|
||||
<div class="input-group">
|
||||
<b-form-input type="text" :placeholder="$t('CategoryName')" v-model="new_category.value" />
|
||||
<b-button class="input-group-append" variant="success" @click="addCategory"><i class="pr-2 pt-1 fas fa-save"></i> {{ $t("Save") }}</b-button>
|
||||
</div>
|
||||
<b-input-group>
|
||||
<b-form-input type="text" class="form-control-append" :placeholder="$t('CategoryName')" v-model="new_category.value" />
|
||||
<b-input-group-append>
|
||||
<b-button class="input-group-append" variant="success" @click="addCategory"><i class="pr-2 pt-1 fas fa-save"></i> {{ $t("Create") }} </b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</b-card>
|
||||
|
||||
<b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3">{{ $t("CategoryInstruction") }}</b-card-sub-title>
|
||||
<b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3">{{ $t("CategoryInstruction") }} </b-card-sub-title>
|
||||
<b-card v-if="new_supermarket.editmode && supermarketCategory.length === 0" class="m-0 p-0 font-weight-bold no-body" border-variant="success" v-bind:key="-1" />
|
||||
<draggable
|
||||
class="list-group"
|
||||
@@ -256,16 +321,29 @@
|
||||
>
|
||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||
<b-card
|
||||
class="m-0 p-0 font-weight-bold no-body list-group-item"
|
||||
no-body
|
||||
v-hover
|
||||
class="mt-1 list-group-item p-2"
|
||||
:style="new_supermarket.editmode ? 'cursor:move' : ''"
|
||||
v-for="c in supermarketCategory"
|
||||
v-bind:key="c.id"
|
||||
:border-variant="new_supermarket.editmode ? 'success' : ''"
|
||||
>
|
||||
{{ categoryName(c) }}
|
||||
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
||||
<i class="btn fas fa-trash fa-lg px-2 text-muted" />
|
||||
</b-button>
|
||||
<b-card-header class="p-2 border-0">
|
||||
<div class="row">
|
||||
<div class="col-2" v-if="new_supermarket.editmode">
|
||||
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
|
||||
</div>
|
||||
<div :class="new_supermarket.editmode ? 'col-10' : 'col-12'">
|
||||
<h5 class="mt-1 mb-1">
|
||||
{{ categoryName(c) }}
|
||||
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
||||
<i class="btn fas fa-trash fa-lg px-2 text-danger" />
|
||||
</b-button>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-header>
|
||||
</b-card>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
@@ -275,18 +353,29 @@
|
||||
class="list-group"
|
||||
:list="notSupermarketCategory"
|
||||
group="category"
|
||||
v-if="new_supermarket.editmode"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
ghost-class="ghost"
|
||||
v-if="new_supermarket.editmode"
|
||||
v-bind="{ animation: 200 }"
|
||||
>
|
||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||
<b-card class="m-0 p-0 font-weight-bold no-body list-group-item" style="cursor: move" v-for="c in notSupermarketCategory" v-bind:key="c.id" :border-variant="'danger'">
|
||||
{{ categoryName(c) }}
|
||||
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
||||
<i class="btn fas fa-trash fa-lg px-2 text-muted" />
|
||||
</b-button>
|
||||
<b-card no-body v-hover class="mt-1 list-group-item p-2" style="cursor: move" v-for="c in notSupermarketCategory" v-bind:key="c.id" :border-variant="'danger'">
|
||||
<b-card-header class="p-2 border-0">
|
||||
<div class="row">
|
||||
<div class="col-2" v-if="new_supermarket.editmode">
|
||||
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v"></i></button>
|
||||
</div>
|
||||
<div :class="new_supermarket.editmode ? 'col-10' : 'col-12'">
|
||||
<h5 class="mt-1 mb-1">
|
||||
{{ categoryName(c) }}
|
||||
<b-button variant="link" class="p-0 m-0 float-right" @click="deleteCategory(c)">
|
||||
<i class="btn fas fa-trash fa-lg px-2 text-primary" />
|
||||
</b-button>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-header>
|
||||
</b-card>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
@@ -297,12 +386,12 @@
|
||||
<!-- settings tab -->
|
||||
<b-tab :title="$t('Settings')">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col col-md-4 col-sm-8">
|
||||
<div class="col-12 col-md-8">
|
||||
<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" />
|
||||
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.mealplan_autoadd_shopping" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -312,14 +401,14 @@
|
||||
</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">{{ $t("mealplan_autoexclude_onhand") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="checkbox" size="sm" v-model="settings.mealplan_autoexclude_onhand" @change="saveSettings" />
|
||||
<input type="checkbox" class="form-control settings-checkbox" 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>
|
||||
<em class="small text-muted">{{ $t("mealplan_autoexclude_onhand_desc") }}</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,7 +416,7 @@
|
||||
<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" />
|
||||
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.mealplan_autoinclude_related" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -343,10 +432,7 @@
|
||||
<div class="col col-md-6 text-right">
|
||||
<generic-multiselect
|
||||
size="sm"
|
||||
@change="
|
||||
settings.shopping_share = $event.val
|
||||
saveSettings()
|
||||
"
|
||||
@change="settings.shopping_share = $event.val;saveSettings()"
|
||||
:model="Models.USER"
|
||||
:initial_selection="settings.shopping_share"
|
||||
label="username"
|
||||
@@ -365,7 +451,7 @@
|
||||
<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" />
|
||||
<input type="number" class="form-control" v-model="settings.shopping_auto_sync" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -378,7 +464,7 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("shopping_add_onhand") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="checkbox" size="sm" v-model="settings.shopping_add_onhand" @change="saveSettings" />
|
||||
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.shopping_add_onhand" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -391,10 +477,9 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("shopping_recent_days") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="number" size="sm" v-model="settings.shopping_recent_days" @change="saveSettings" />
|
||||
<input type="number" class="form-control" v-model="settings.shopping_recent_days" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">
|
||||
@@ -405,7 +490,7 @@
|
||||
<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" />
|
||||
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.filter_to_supermarket" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -418,7 +503,7 @@
|
||||
<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" />
|
||||
<input type="number" class="form-control" min="1" v-model="settings.default_delay" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -431,7 +516,7 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("csv_delim_label") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="string" size="sm" v-model="settings.csv_delim" @change="saveSettings" />
|
||||
<input class="form-control" v-model="settings.csv_delim" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -444,7 +529,7 @@
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("csv_prefix_label") }}</div>
|
||||
<div class="col col-md-6 text-right">
|
||||
<input type="string" size="sm" v-model="settings.csv_prefix" @change="saveSettings" />
|
||||
<input class="form-control" v-model="settings.csv_prefix" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
@@ -454,6 +539,19 @@
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-6">{{ $t("left_handed") }}</div>
|
||||
<div class="col col-md-6">
|
||||
<input type="checkbox" class="form-control settings-checkbox" v-model="settings.left_handed" @change="saveSettings" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row sm mb-3">
|
||||
<div class="col">
|
||||
<em class="small text-muted">
|
||||
{{ $t("left_handed_help") }}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -480,30 +578,31 @@
|
||||
</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="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>
|
||||
<ContextMenuItem>
|
||||
<b-input-group>
|
||||
<template #prepend>
|
||||
<span class="dropdown-item p-2 text-decoration-none" style="user-select: none !important"><i class="fas fa-cubes"></i> {{ $t("MoveCategory") }}</span>
|
||||
</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>
|
||||
<b-form-select
|
||||
class="form-control mt-1 mr-1"
|
||||
:options="shopping_categories"
|
||||
text-field="name"
|
||||
value-field="id"
|
||||
v-model="shopcat"
|
||||
@change="
|
||||
moveEntry($event, contextData)
|
||||
$refs.menu.close()
|
||||
"
|
||||
></b-form-select>
|
||||
</b-input-group>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
$refs.menu.close()
|
||||
@@ -518,14 +617,7 @@
|
||||
delayThis(contextData)
|
||||
"
|
||||
>
|
||||
<b-form-group label-cols="9" content-cols="3" 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>
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-hourglass"></i> {{ $t("DelayFor", { hours: delay }) }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
@@ -535,7 +627,6 @@
|
||||
>
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-check-square"></i> {{ $t("mark_complete") }}</a>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
@click="
|
||||
$refs.menu.close()
|
||||
@@ -546,6 +637,26 @@
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
<transition name="slided-fade">
|
||||
<div class="row fixed-bottom p-2 b-1 border-top text-center d-flex d-md-none" style="background: rgba(255, 255, 255, 0.6)" v-if="current_tab === 0">
|
||||
<div class="col-6">
|
||||
<a class="btn btn-block btn-success shadow-none" @click="entrymode = !entrymode"
|
||||
><i class="fas fa-cart-plus"></i>
|
||||
{{ $t("New Entry") }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<b-dropdown id="dropdown-dropup" block dropup variant="primary" class="shadow-none">
|
||||
<template #button-content> <i class="fas fa-download"></i> {{ $t("Export") }} </template>
|
||||
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
||||
<DownloadCSV :items="csvData" :delim="settings.csv_delim" name="shopping.csv" :label="$t('download_csv')" icon="fas fa-file-csv" />
|
||||
<CopyToClipboard :items="csvData" :settings="settings" :label="$t('copy_to_clipboard')" icon="fas fa-clipboard-list" />
|
||||
<CopyToClipboard :items="csvData" :settings="settings" format="table" :label="$t('copy_markdown_table')" icon="fab fa-markdown" />
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<shopping-modal v-if="new_recipe.id" :recipe="new_recipe" :servings="parseInt(add_recipe_servings)" :modal_id="new_recipe.id" @finish="finishShopping" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -564,6 +675,7 @@ import CopyToClipboard from "@/components/Buttons/CopyToClipboard"
|
||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||
import GenericPill from "@/components/GenericPill"
|
||||
import LookupInput from "@/components/Modals/LookupInput"
|
||||
import ShoppingModal from "@/components/Modals/ShoppingModal"
|
||||
import draggable from "vuedraggable"
|
||||
|
||||
import { ApiMixin, getUserPreference, StandardToasts, makeToast } from "@/utils/utils"
|
||||
@@ -576,12 +688,25 @@ let SETTINGS_COOKIE_NAME = "shopping_settings"
|
||||
export default {
|
||||
name: "ShoppingListView",
|
||||
mixins: [ApiMixin],
|
||||
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput, DownloadPDF, DownloadCSV, CopyToClipboard },
|
||||
components: {
|
||||
ContextMenu,
|
||||
ContextMenuItem,
|
||||
ShoppingLineItem,
|
||||
GenericMultiselect,
|
||||
GenericPill,
|
||||
draggable,
|
||||
LookupInput,
|
||||
DownloadPDF,
|
||||
DownloadCSV,
|
||||
CopyToClipboard,
|
||||
ShoppingModal,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
items: [],
|
||||
current_tab: 0,
|
||||
group_by: "category",
|
||||
group_by_choices: ["created_by", "category", "recipe"],
|
||||
supermarkets: [],
|
||||
@@ -604,11 +729,13 @@ export default {
|
||||
csv_delim: ",",
|
||||
csv_prefix: undefined,
|
||||
shopping_add_onhand: true,
|
||||
left_handed: false,
|
||||
},
|
||||
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
||||
new_category: { entrymode: false, value: undefined },
|
||||
autosync_id: undefined,
|
||||
auto_sync_running: false,
|
||||
auto_sync_running: false, // track to not start a new sync before old one was finished
|
||||
auto_sync_blocked: false, // blocking auto sync while request to check item is still running
|
||||
show_delay: false,
|
||||
drag: false,
|
||||
show_modal: false,
|
||||
@@ -617,6 +744,10 @@ export default {
|
||||
entrymode: false,
|
||||
new_item: { amount: 1, unit: undefined, food: undefined, ingredient: undefined },
|
||||
online: true,
|
||||
new_recipe: {
|
||||
id: undefined,
|
||||
},
|
||||
add_recipe_servings: 1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -654,6 +785,9 @@ export default {
|
||||
|
||||
var groups = { false: {}, true: {} } // force unchecked to always be first
|
||||
if (this.selected_supermarket) {
|
||||
// TODO: make nulls_first a user setting
|
||||
groups.false[this.$t("Undefined")] = {}
|
||||
groups.true[this.$t("Undefined")] = {}
|
||||
let super_cats = this.supermarkets
|
||||
.filter((x) => x.id === this.selected_supermarket)
|
||||
.map((x) => x.category_to_supermarket)
|
||||
@@ -664,9 +798,6 @@ export default {
|
||||
groups["true"][cat] = {}
|
||||
})
|
||||
} else {
|
||||
// TODO: make nulls_first a user setting
|
||||
groups.false[this.$t("Undefined")] = {}
|
||||
groups.true[this.$t("Undefined")] = {}
|
||||
this.shopping_categories.forEach((cat) => {
|
||||
groups.false[cat.name] = {}
|
||||
groups.true[cat.name] = {}
|
||||
@@ -715,7 +846,8 @@ export default {
|
||||
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()]
|
||||
// hiding recipes associated with shopping list items that are complete
|
||||
return [...new Map(this.items.filter((x) => x.list_recipe && !x.checked).map((item) => [item["list_recipe"], item])).values()]
|
||||
},
|
||||
supermarketCategory() {
|
||||
return this.new_supermarket.editmode ? this.new_supermarket.value.category_to_supermarket : this.shopping_categories
|
||||
@@ -739,6 +871,13 @@ export default {
|
||||
watch: {
|
||||
selected_supermarket(newVal, oldVal) {
|
||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||
localStorage.setItem("shopping_v2_selected_supermarket", JSON.stringify(this.selected_supermarket))
|
||||
},
|
||||
new_recipe: {
|
||||
handler() {
|
||||
this.add_recipe_servings = this.new_recipe.servings
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
"settings.filter_to_supermarket": function (newVal, oldVal) {
|
||||
this.supermarket_categories_only = this.settings.filter_to_supermarket
|
||||
@@ -784,20 +923,28 @@ export default {
|
||||
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
|
||||
this.entry_mode_simple = this.$cookies.get(SETTINGS_COOKIE_NAME)
|
||||
}
|
||||
this.selected_supermarket = localStorage.getItem("shopping_v2_selected_supermarket") || undefined
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
addItem: function () {
|
||||
if (this.entry_mode_simple) {
|
||||
this.genericPostAPI("api_ingredient_from_string", { text: this.new_item.ingredient }).then((result) => {
|
||||
this.new_item = {
|
||||
amount: result.data.amount,
|
||||
unit: { name: result.data.unit },
|
||||
food: { name: result.data.food },
|
||||
}
|
||||
this.addEntry()
|
||||
})
|
||||
if (this.new_item.ingredient !== "" && this.new_item.ingredient !== undefined) {
|
||||
this.genericPostAPI("api_ingredient_from_string", { text: this.new_item.ingredient }).then((result) => {
|
||||
let unit = null
|
||||
if (result.data.unit !== "") {
|
||||
unit = { name: result.data.unit }
|
||||
}
|
||||
|
||||
this.new_item = {
|
||||
amount: result.data.amount,
|
||||
unit: unit,
|
||||
food: { name: result.data.food },
|
||||
}
|
||||
this.addEntry()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.addEntry()
|
||||
}
|
||||
@@ -949,12 +1096,15 @@ export default {
|
||||
if (!autosync) {
|
||||
if (results.data?.length) {
|
||||
this.items = results.data
|
||||
console.log(this.items)
|
||||
} else {
|
||||
console.log("no data returned")
|
||||
}
|
||||
this.loading = false
|
||||
} else {
|
||||
this.mergeShoppingList(results.data)
|
||||
if (!this.auto_sync_blocked) {
|
||||
this.mergeShoppingList(results.data)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -983,6 +1133,15 @@ export default {
|
||||
})
|
||||
)
|
||||
this.auto_sync_running = false
|
||||
let new_entries = data.map((x) => x.id).filter((y) => !this.items.map((z) => z.id).includes(y))
|
||||
if (new_entries.length > 0) {
|
||||
let api = new ApiApiFactory()
|
||||
new_entries.forEach((new_id) => {
|
||||
api.retrieveShoppingListEntry(new_id).then((result) => {
|
||||
this.items.push(result.data)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
moveEntry: function (e, item) {
|
||||
if (!e) {
|
||||
@@ -1077,6 +1236,7 @@ export default {
|
||||
},
|
||||
updateChecked: function (update) {
|
||||
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
|
||||
this.auto_sync_blocked = true
|
||||
let promises = []
|
||||
update.entries.forEach((x) => {
|
||||
const id = x?.id ?? x
|
||||
@@ -1091,15 +1251,20 @@ export default {
|
||||
Vue.set(item, "completed_at", completed_at)
|
||||
})
|
||||
|
||||
Promise.all(promises).catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.auto_sync_blocked = false
|
||||
})
|
||||
.catch((err) => {
|
||||
this.auto_sync_blocked = false
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
},
|
||||
updateFood: function (food, field) {
|
||||
let api = new ApiApiFactory()
|
||||
if (field) {
|
||||
// assume if field is changing it should no longer be inheritted
|
||||
// assume if field is changing it should no longer be inherited
|
||||
food.inherit_fields = food.inherit_fields.filter((x) => x.field !== field)
|
||||
}
|
||||
|
||||
@@ -1233,6 +1398,24 @@ export default {
|
||||
window.removeEventListener("online", this.updateOnlineStatus)
|
||||
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||
},
|
||||
addRecipeToShopping() {
|
||||
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
|
||||
},
|
||||
finishShopping() {
|
||||
this.getShoppingList()
|
||||
},
|
||||
},
|
||||
directives: {
|
||||
hover: {
|
||||
inserted: function (el) {
|
||||
el.addEventListener("mouseenter", () => {
|
||||
el.classList.add("shadow")
|
||||
})
|
||||
el.addEventListener("mouseleave", () => {
|
||||
el.classList.remove("shadow")
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1245,15 +1428,18 @@ export default {
|
||||
-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;
|
||||
@@ -1263,4 +1449,35 @@ export default {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter, .slide-fade-leave-to
|
||||
/* .slider-fade-leave-active below version 2.1.8 */ {
|
||||
transform: translateX(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.form-control-append {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#shoppinglist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
height: 65vh;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-checkbox {
|
||||
font-size: 0.3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user