Merge branch 'feature/export-progress' into develop

This commit is contained in:
vabene1111
2022-01-28 15:41:49 +01:00
committed by GitHub
141 changed files with 14841 additions and 8379 deletions

View File

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

View File

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

View File

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

View File

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

View File

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