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>

View File

@ -1,122 +1,118 @@
<template>
<div>
<b-modal class="modal" :id="`id_modal_add_book_${modal_id}`" :title="$t('Manage_Books')" :ok-title="$t('Add')" :cancel-title="$t('Close')" @ok="addToBook()" @shown="loadBookEntries">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center" v-for="be in this.recipe_book_list" v-bind:key="be.id">
{{ be.book_content.name }} <span class="btn btn-sm btn-danger" @click="removeFromBook(be)"><i class="fa fa-trash-alt"></i></span>
</li>
</ul>
<div>
<b-modal class="modal" :id="`id_modal_add_book_${modal_id}`" :title="$t('Manage_Books')" :ok-title="$t('Add')"
:cancel-title="$t('Close')" @ok="addToBook()" @shown="loadBookEntries">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center" v-for="be in this.recipe_book_list" v-bind:key="be.id">
{{ be.book_content.name }} <span class="btn btn-sm btn-danger" @click="removeFromBook(be)"><i class="fa fa-trash-alt"></i></span>
</li>
</ul>
<multiselect
style="margin-top: 1vh"
v-model="selected_book"
:options="books_filtered"
:taggable="true"
@tag="createBook"
v-bind:tag-placeholder="$t('Create')"
:placeholder="$t('Select_Book')"
label="name"
track-by="id"
id="id_books"
:multiple="false"
:loading="books_loading"
@search-change="loadBooks">
</multiselect>
</b-modal>
</div>
<multiselect
style="margin-top: 1vh"
v-model="selected_book"
:options="books_filtered"
:taggable="true"
@tag="createBook"
v-bind:tag-placeholder="$t('Create')"
:placeholder="$t('Select_Book')"
label="name"
track-by="id"
id="id_books"
:multiple="false"
:internal-search="false"
:loading="books_loading"
@search-change="loadBooks"
>
</multiselect>
</b-modal>
</div>
</template>
<script>
import Multiselect from "vue-multiselect"
import Multiselect from 'vue-multiselect'
import moment from 'moment'
import moment from "moment"
Vue.prototype.moment = moment
import Vue from "vue";
import {BootstrapVue} from "bootstrap-vue";
import {ApiApiFactory} from "@/utils/openapi/api";
import {makeStandardToast, StandardToasts} from "@/utils/utils";
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import { ApiApiFactory } from "@/utils/openapi/api"
import { makeStandardToast, StandardToasts } from "@/utils/utils"
Vue.use(BootstrapVue)
export default {
name: 'AddRecipeToBook',
components: {
Multiselect
},
props: {
recipe: Object,
modal_id: Number
},
data() {
return {
books: [],
books_loading: false,
recipe_book_list: [],
selected_book: null,
}
},
computed: {
books_filtered: function () {
let books_filtered = []
this.books.forEach(b => {
if (this.recipe_book_list.filter(e => e.book === b.id).length === 0) {
books_filtered.push(b)
name: "AddRecipeToBook",
components: {
Multiselect,
},
props: {
recipe: Object,
modal_id: Number,
},
data() {
return {
books: [],
books_loading: false,
recipe_book_list: [],
selected_book: null,
}
})
},
computed: {
books_filtered: function () {
let books_filtered = []
return books_filtered
}
},
mounted() {
this.books.forEach((b) => {
if (this.recipe_book_list.filter((e) => e.book === b.id).length === 0) {
books_filtered.push(b)
}
})
},
methods: {
loadBooks: function (query) {
this.books_loading = true
let apiFactory = new ApiApiFactory()
apiFactory.listRecipeBooks({query: {query: query}}).then(results => {
this.books = results.data.filter(e => this.recipe_book_list.indexOf(e) === -1)
this.books_loading = false
})
return books_filtered
},
},
createBook: function (name) {
let apiFactory = new ApiApiFactory()
apiFactory.createRecipeBook({name: name}).then(r => {
this.books.push(r.data)
this.selected_book = r.data
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
})
mounted() {},
methods: {
loadBooks: function (query) {
this.books_loading = true
let apiFactory = new ApiApiFactory()
apiFactory.listRecipeBooks({ query: { query: query } }).then((results) => {
this.books = results.data.filter((e) => this.recipe_book_list.indexOf(e) === -1)
this.books_loading = false
})
},
createBook: function (name) {
let apiFactory = new ApiApiFactory()
apiFactory.createRecipeBook({ name: name }).then((r) => {
this.books.push(r.data)
this.selected_book = r.data
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
})
},
addToBook: function () {
let apiFactory = new ApiApiFactory()
apiFactory.createRecipeBookEntry({ book: this.selected_book.id, recipe: this.recipe.id }).then((r) => {
this.recipe_book_list.push(r.data)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
})
},
removeFromBook: function (book_entry) {
let apiFactory = new ApiApiFactory()
apiFactory.destroyRecipeBookEntry(book_entry.id).then((r) => {
this.recipe_book_list = this.recipe_book_list.filter((e) => e.id !== book_entry.id)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
})
},
loadBookEntries: function () {
let apiFactory = new ApiApiFactory()
apiFactory.listRecipeBookEntrys({ query: { recipe: this.recipe.id } }).then((r) => {
this.recipe_book_list = r.data
this.loadBooks("")
})
},
},
addToBook: function () {
let apiFactory = new ApiApiFactory()
apiFactory.createRecipeBookEntry({book: this.selected_book.id, recipe: this.recipe.id}).then(r => {
this.recipe_book_list.push(r.data)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
})
},
removeFromBook: function (book_entry) {
let apiFactory = new ApiApiFactory()
apiFactory.destroyRecipeBookEntry(book_entry.id).then(r => {
this.recipe_book_list = this.recipe_book_list.filter(e => e.id !== book_entry.id)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
})
},
loadBookEntries: function () {
let apiFactory = new ApiApiFactory()
apiFactory.listRecipeBookEntrys({query: {recipe: this.recipe.id}}).then(r => {
this.recipe_book_list = r.data
this.loadBooks('')
})
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>

View File

@ -0,0 +1,58 @@
<template>
<span><i class="mx-1 far fa-question-circle text-muted" @click="this_help.show = !this_help.show" /></span>
</template>
<script>
import Vue from "vue"
import VueCookies from "vue-cookies"
Vue.use(VueCookies)
let HELP_COOKIE_NAME = "help_settings"
export default {
name: "HelpBadge",
props: {
component: { type: String, required: true },
},
data() {
return {
help: {},
default: {
show: true,
},
this_help: undefined,
}
},
mounted() {
this.$nextTick(function () {
if (this.$cookies.isKey(HELP_COOKIE_NAME)) {
this.help = Object.assign({}, this.help, this.$cookies.get(HELP_COOKIE_NAME))
}
this.this_help = Object.assign({}, this.default, this.help?.[this.component])
})
},
watch: {
help: {
handler() {
this.$cookies.set(HELP_COOKIE_NAME, this.help)
},
deep: true,
},
this_help: {
handler() {
this.help[this.component] = Object.assign({}, this.this_help)
this.$cookies.set(HELP_COOKIE_NAME, this.help)
},
deep: true,
},
"this_help.show": function () {
if (this.this_help.show) {
this.$emit("show")
} else {
this.$emit("hide")
}
},
},
methods: {},
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<span>
<span v-if="!item.ignore_shopping">
<b-button class="btn text-decoration-none px-1 border-0" variant="link" :id="`shopping${item.id}`" @click="addShopping()">
<i
class="fas"
@ -59,7 +59,7 @@ export default {
addShopping() {
if (this.shopping) {
return
} // if item already in shopping list, excution handled after confirmation
} // if item already in shopping list, execution handled after confirmation
let params = {
id: this.item.id,
amount: 1,

View File

@ -1,5 +1,5 @@
<template>
<div>
<div style="cursor:pointer">
<a v-if="!button" class="dropdown-item" @click="clipboard"><i :class="icon"></i> {{ label }}</a>
<b-button v-if="button" @click="clipboard">{{ label }}</b-button>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div>
<div style="cursor:pointer">
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div>
<div style="cursor:pointer">
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
</div>

View File

@ -1,45 +1,75 @@
<template>
<div v-if="recipes !== {}">
<div id="switcher" class="align-center">
<i class="btn btn-outline-dark fas fa-receipt fa-xl fa-fw shadow-none btn-circle"
<i class="btn btn-primary fas fa-receipt fa-xl fa-fw shadow-none btn-circle"
v-b-toggle.related-recipes/>
</div>
<b-sidebar id="related-recipes" title="Quick actions" backdrop right shadow="sm" style="z-index: 10000">
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000"
@shown="updatePinnedRecipes()">
<template #default="{ hide }">
<nav class="mb-3 ml-3">
<b-nav vertical>
<h5><i class="fas fa-calendar fa-fw"></i> Planned</h5>
<div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end">
<div v-for="r in planned_recipes" :key="`plan${r.id}`">
<b-nav-item variant="link" @click="
navRecipe(r)
hide()
">{{ r.name }}
</b-nav-item>
</div>
<hr/>
<h5><i class="fas fa-thumbtack fa-fw"></i> Pinned</h5>
<h5>Planned <i class="fas fa-calendar fa-fw"></i></h5>
<div v-for="r in pinned_recipes" :key="`pin${r.id}`">
<b-nav-item variant="link" @click="
navRecipe(r)
hide()
">{{ r.name }}
</b-nav-item>
</div>
<hr/>
<h5><i class="fas fa-link fa-fw"></i> Related</h5>
<div class="text-right">
<template v-if="planned_recipes.length > 0">
<div v-for="r in planned_recipes" :key="`plan${r.id}`">
<div class="pb-1 pt-1">
<a @click=" navRecipe(r); hide()" href="javascript:void(0);">{{ r.name }}</a>
</div>
</div>
</template>
<template v-else>
<span class="text-muted">You have nothing planned for today!</span>
</template>
</div>
<div v-for="r in related_recipes" :key="`related${r.id}`">
<b-nav-item variant="link" @click="
navRecipe(r)
hide()
">{{ r.name }}
</b-nav-item>
<h5>Pinned <i class="fas fa-thumbtack fa-fw"></i></h5>
<template v-if="pinned_recipes.length > 0">
<div class="text-right">
<div v-for="r in pinned_recipes" :key="`pin${r.id}`">
<b-row class="pb-1 pt-1">
<b-col cols="2">
<a href="javascript:void(0)" @click="unPinRecipe(r)"
class="text-muted"><i class="fas fa-times"></i></a>
</b-col>
<b-col cols="10">
<a @click="navRecipe(r); hide()" href="javascript:void(0);"
class="align-self-end">{{ r.name }} </a>
</b-col>
</b-row>
</div>
</div>
</b-nav>
</nav>
</template>
<template v-else>
<span class="text-muted">You have no pinned recipes!</span>
</template>
<template v-if="related_recipes.length > 0">
<h5>Related <i class="fas fa-link fa-fw"></i></h5>
<div class="text-right">
<div v-for="r in related_recipes" :key="`related${r.id}`">
<div class="pb-1 pt-1">
<a @click=" navRecipe(r); hide()" href="javascript:void(0);">{{ r.name }}</a>
</div>
</div>
</div>
</template>
</div>
</template>
<template #footer="{ hide }">
<div class="d-flex bg-dark text-light align-items-center px-3 py-2">
<strong class="mr-auto">Quick actions</strong>
<b-button size="sm" @click="hide">Close</b-button>
</div>
</template>
</b-sidebar>
</div>
@ -60,7 +90,7 @@ export default {
related_recipes: [],
planned_recipes: [],
pinned_recipes: [],
recipes: {}
recipes: {},
}
},
computed: {
@ -89,14 +119,22 @@ export default {
window.location.href = this.resolveDjangoUrl("view_recipe", recipe.id)
}
},
updatePinnedRecipes: function () {
//TODO clean this up to prevent duplicate API calls
this.loadPinnedRecipes()
this.loadRecipeData()
},
loadRecipeData: function () {
let apiClient = new ApiApiFactory()
let recipe_list = [...this.related_recipes, ...this.planned_recipes, ...this.pinned_recipes]
let recipe_ids = []
recipe_list.forEach((recipe) => {
if (!recipe_ids.includes(recipe.id)) {
recipe_ids.push(recipe.id)
let id = recipe.id
if (!recipe_ids.includes(id)) {
recipe_ids.push(id)
}
})
@ -111,12 +149,15 @@ export default {
let apiClient = new ApiApiFactory()
// get related recipes and save them for later
return apiClient.relatedRecipe(this.recipe, {query: {levels: 2}}).then((result) => {
this.related_recipes = result.data
})
if (this.$parent.recipe) {
this.related_recipes = [this.$parent.recipe]
return apiClient.relatedRecipe(this.$parent.recipe.id, {query: {levels: 2, format: 'json'}}).then((result) => {
this.related_recipes = this.related_recipes.concat(result.data)
})
}
},
loadPinnedRecipes: function () {
let pinned_recipe_ids = localStorage.getItem('pinned_recipes') || []
let pinned_recipe_ids = JSON.parse(localStorage.getItem('pinned_recipes')) || []
this.pinned_recipes = pinned_recipe_ids
},
loadMealPlans: function () {
@ -142,6 +183,13 @@ export default {
return Promise.all(promises)
})
},
unPinRecipe: function (recipe) {
let pinnedRecipes = JSON.parse(localStorage.getItem('pinned_recipes')) || []
pinnedRecipes = pinnedRecipes.filter((r) => r.id !== recipe.id)
console.log('pinned left', pinnedRecipes)
this.pinned_recipes = pinnedRecipes
localStorage.setItem('pinned_recipes', JSON.stringify(pinnedRecipes))
}
},
}
</script>

View File

@ -5,21 +5,21 @@
<template #button-content>
<i class="fas fa-chevron-down"></i>
</template>
<b-dropdown-item :href="resolveDjangoUrl('list_food')"> <i class="fas fa-leaf fa-fw"></i> {{ Models["FOOD"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_food')"> <i class="fas fa-leaf fa-fw"></i> {{ $t(Models["FOOD"].name) }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_keyword')"> <i class="fas fa-tags fa-fw"></i> {{ Models["KEYWORD"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_keyword')"> <i class="fas fa-tags fa-fw"></i> {{ $t(Models["KEYWORD"].name) }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_unit')"> <i class="fas fa-balance-scale fa-fw"></i> {{ Models["UNIT"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_unit')"> <i class="fas fa-balance-scale fa-fw"></i> {{ $t(Models["UNIT"].name) }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket')"> <i class="fas fa-store-alt fa-fw"></i> {{ Models["SUPERMARKET"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket')"> <i class="fas fa-store-alt fa-fw"></i> {{ $t(Models["SUPERMARKET"].name) }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket_category')"> <i class="fas fa-cubes fa-fw"></i> {{ Models["SHOPPING_CATEGORY"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_supermarket_category')"> <i class="fas fa-cubes fa-fw"></i> {{ $t(Models["SHOPPING_CATEGORY"].name) }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_automation')"> <i class="fas fa-robot fa-fw"></i> {{ Models["AUTOMATION"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_automation')"> <i class="fas fa-robot fa-fw"></i> {{ $t(Models["AUTOMATION"].name) }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_user_file')"> <i class="fas fa-file fa-fw"></i> {{ Models["USERFILE"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_user_file')"> <i class="fas fa-file fa-fw"></i> {{ $t(Models["USERFILE"].name) }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_step')"> <i class="fas fa-puzzle-piece fa-fw"></i>{{ Models["STEP"].name }} </b-dropdown-item>
<b-dropdown-item :href="resolveDjangoUrl('list_step')"> <i class="fas fa-puzzle-piece fa-fw"></i>{{ $t(Models["STEP"].name) }} </b-dropdown-item>
</b-dropdown>
</span>
</template>

View File

@ -0,0 +1,86 @@
// code taken from https://github.com/bootstrap-vue/bootstrap-vue/issues/4977#issuecomment-740215609 and modified
<template>
<b-input-group>
<b-input-group-prepend>
<b-button variant="outline-primary" class="py-0 px-2" size="sm" @click="valueChange(value - 1)">
<b-icon icon="dash" font-scale="1.6" />
</b-button>
</b-input-group-prepend>
<b-form-input
style="text-align: right; border-width: 0px; border: none; padding: 0px; padding-left: 0.5vw; padding-right: 8px; width: 50px"
variant="outline-primary"
:size="size"
:value="value"
type="number"
min="0"
class="border-secondary text-center"
number
@update="valueChange"
/>
<b-input-group-append>
<b-button variant="outline-primary" class="py-0 px-2" size="sm" @click="valueChange(value + 1)">
<b-icon icon="plus" font-scale="1.6" />
</b-button>
</b-input-group-append>
</b-input-group>
</template>
<script>
import { BIcon, BIconDash, BIconPlus } from 'bootstrap-vue'
export default {
name: 'CustomInputSpinButton',
components: {
BIcon,
/* eslint-disable vue/no-unused-components */
BIconDash,
BIconPlus
},
props: {
size: {
type: String,
required: false,
default: 'md',
validator: function (value) {
return ['sm', 'md', 'lg'].includes(value)
}
},
value: {
type: Number,
required: true
}
},
methods: {
valueChange (newValue) {
if (newValue <= 0) {
this.$emit('input', 0)
} else {
this.$emit('input', newValue)
}
}
}
}
</script>
<style scoped>
/* Remove up and down arrows inside number input */
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
</style>

View File

@ -8,12 +8,12 @@
:class="{ 'border border-primary': over, shake: isError }"
:style="{ 'cursor:grab': useDrag }"
:draggable="useDrag"
@[useDrag&&`dragover`].prevent
@[useDrag&&`dragenter`].prevent
@[useDrag&&`dragstart`]="handleDragStart($event)"
@[useDrag&&`dragenter`]="handleDragEnter($event)"
@[useDrag&&`dragleave`]="handleDragLeave($event)"
@[useDrag&&`drop`]="handleDragDrop($event)"
@[useDrag&&`dragover`||``].prevent
@[useDrag&&`dragenter`||``].prevent
@[useDrag&&`dragstart`||``]="handleDragStart($event)"
@[useDrag&&`dragenter`||``]="handleDragEnter($event)"
@[useDrag&&`dragleave`||``]="handleDragLeave($event)"
@[useDrag&&`drop`||``]="handleDragDrop($event)"
>
<b-row no-gutters>
<b-col no-gutters class="col-sm-3">
@ -27,6 +27,7 @@
<div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div>
<generic-pill v-for="x in itemTags" :key="x.field" :item_list="itemList(x)" :label="x.label" :color="x.color" />
<generic-ordered-pill
v-for="x in itemOrderedTags"
:key="x.field"
@ -37,6 +38,7 @@
:item="item"
@finish-action="finishAction"
/>
<div class="mt-auto mb-1" align="right">
<span v-if="item[child_count]" class="mx-2 btn btn-link btn-sm" style="z-index: 800" v-on:click="$emit('item-action', { action: 'get-children', source: item })">
<div v-if="!item.show_children">{{ item[child_count] }} {{ itemName }}</div>
@ -238,7 +240,6 @@ export default {
})
popper.update()
this.over = false
this.$emit({ action: "drop", target: this.item, source: this.source })
} else {
this.isError = true
}

View File

@ -6,6 +6,8 @@
:clear-on-select="true"
:hide-selected="multiple"
:preserve-search="true"
:internal-search="false"
:limit="limit"
:placeholder="lookupPlaceholder"
:label="label"
track-by="id"
@ -87,8 +89,9 @@ export default {
search: function (query) {
let options = {
page: 1,
pageSize: 10,
pageSize: this.limit,
query: query,
limit: this.limit,
}
this.genericAPI(this.model, this.Actions.LIST, options).then((result) => {
this.objects = this.sticky_options.concat(result.data?.results ?? result.data)
@ -113,7 +116,7 @@ export default {
},
addNew(e) {
this.$emit("new", e)
// could refactor as Promise - seems unecessary
// could refactor as Promise - seems unnecessary
setTimeout(() => {
this.search("")
}, 750)

View File

@ -34,6 +34,7 @@
</td>
<td v-else-if="show_shopping" class="text-right text-nowrap">
<b-button
v-if="!ingredient.food.ignore_shopping"
class="btn text-decoration-none fas fa-shopping-cart px-2 user-select-none"
variant="link"
v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }"
@ -43,10 +44,10 @@
'text-warning': shopping_status === null,
}"
/>
<span class="px-2">
<span v-if="!ingredient.food.ignore_shopping" class="px-2">
<input type="checkbox" class="align-middle" v-model="shop" @change="changeShopping" />
</span>
<on-hand-badge :item="ingredient.food" />
<on-hand-badge v-if="!ingredient.food.ignore_shopping" :item="ingredient.food" />
</td>
</template>
</tr>
@ -100,10 +101,10 @@ export default {
filtered_list = filtered_list.filter((x) => x.list_recipe == this.recipe_list)
}
// how many ShoppingListRecipes are there for this recipe?
let count_shopping_recipes = [...new Set(filtered_list.map((x) => x.list_recipe))].length
let count_shopping_recipes = [...new Set(filtered_list.filter((x) => x.list_recipe))].length
let count_shopping_ingredient = filtered_list.filter((x) => x.ingredient == this.ingredient.id).length
if (count_shopping_recipes >= 1) {
if (count_shopping_recipes >= 1 && this.recipe_list) {
// This recipe is in the shopping list
this.shop = false // don't check any boxes until user selects a shopping list to edit
if (count_shopping_ingredient >= 1) {
@ -117,7 +118,7 @@ export default {
} else {
// there are not recipes in the shopping list
// set default value
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe && !this.ingredient?.food?.ignore_shopping
this.$emit("add-to-shopping", { item: this.ingredient, add: this.shop })
// mark checked if the food is in the shopping list for this ingredient/recipe
if (count_shopping_ingredient >= 1) {
@ -135,7 +136,7 @@ export default {
if (this.add_shopping_mode) {
// if we are in add shopping mode (e.g. recipe_shopping_modal) start with all checks marked
// except if on_hand (could be if recipe too?)
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe
this.shop = !this.ingredient?.food?.food_onhand && !this.ingredient?.food?.recipe && !this.ingredient?.food?.ignore_shopping
}
},
},

View File

@ -24,6 +24,11 @@
<table class="table table-sm">
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
<template v-for="s in steps">
<tr v-bind:key="s.id" v-if="s.show_as_header && s.name !== '' && !add_shopping_mode">
<td colspan="5">
<b>{{ s.name }}</b>
</td>
</tr>
<template v-for="i in s.ingredients">
<ingredient-component
:ingredient="i"
@ -54,6 +59,7 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
import IngredientComponent from "@/components/IngredientComponent"
import { ApiMixin, StandardToasts } from "@/utils/utils"
Vue.use(BootstrapVue)
export default {
@ -87,7 +93,12 @@ export default {
// returns open shopping lists associated with this recipe
let recipe_in_list = this.shopping_list
.map((x) => {
return { value: x?.list_recipe, text: x?.recipe_mealplan?.name, recipe: x?.recipe_mealplan?.recipe ?? 0, servings: x?.recipe_mealplan?.servings }
return {
value: x?.list_recipe,
text: x?.recipe_mealplan?.name,
recipe: x?.recipe_mealplan?.recipe ?? 0,
servings: x?.recipe_mealplan?.servings,
}
})
.filter((x) => x?.recipe == this.recipe)
return [...new Map(recipe_in_list.map((x) => [x["value"], x])).values()] // filter to unique lists

View File

@ -1,17 +1,22 @@
<template>
<div v-if="recipe.keywords.length > 0">
<span :key="k.id" v-for="k in recipe.keywords" class="pl-1">
<b-badge pill variant="light" class="font-weight-normal">{{k.label}}</b-badge>
<a :href="`${resolveDjangoUrl('view_search')}?keyword=${k.id}`"><b-badge pill variant="light"
class="font-weight-normal">{{ k.label }}</b-badge></a>
</span>
</div>
</template>
<script>
import {ResolveUrlMixin} from "@/utils/utils";
export default {
name: 'KeywordsComponent',
props: {
recipe: Object,
},
name: 'KeywordsComponent',
mixins: [ResolveUrlMixin],
props: {
recipe: Object,
},
}
</script>

View File

@ -81,7 +81,8 @@
</b-input-group>
</div>
<div class="col-lg-6 d-none d-lg-block d-xl-block">
<recipe-card :recipe="entryEditing.recipe" v-if="entryEditing.recipe != null" :detailed="false"></recipe-card>
<recipe-card v-if="entryEditing.recipe && !entryEditing.addshopping" :recipe="entryEditing.recipe" :detailed="false"></recipe-card>
<ingredients-card v-if="entryEditing.recipe && entryEditing.addshopping" :recipe="entryEditing.recipe" :detailed="false"></ingredients-card>
</div>
</div>
<div class="row mt-3 mb-3">
@ -104,7 +105,6 @@ import GenericMultiselect from "@/components/GenericMultiselect"
import { ApiMixin, getUserPreference } from "@/utils/utils"
const { ApiApiFactory } = require("@/utils/openapi/api")
const { StandardToasts } = require("@/utils/utils")
Vue.use(BootstrapVue)
@ -130,6 +130,7 @@ export default {
components: {
GenericMultiselect,
RecipeCard: () => import("@/components/RecipeCard.vue"),
IngredientsCard: () => import("@/components/IngredientsCard.vue"),
},
data() {
return {

View File

@ -1,34 +1,34 @@
<template>
<div>
<b-form-checkbox v-model="new_value">{{label}}</b-form-checkbox>
<b-form-checkbox v-model="new_value">{{ label }}</b-form-checkbox>
<em v-if="help" class="small text-muted">{{ help }}</em>
</div>
</template>
<script>
export default {
name: 'CheckboxInput',
props: {
field: {type: String, default: 'You Forgot To Set Field Name'},
label: {type: String, default: 'Checkbox Field'},
value: {type: Boolean, default: false},
show_move: {type: Boolean, default: false},
show_merge: {type: Boolean, default: false},
},
data() {
return {
new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
'new_value': function () {
this.$root.$emit('change', this.field, this.new_value)
name: "CheckboxInput",
props: {
field: { type: String, default: "You Forgot To Set Field Name" },
label: { type: String, default: "Checkbox Field" },
value: { type: Boolean, default: false },
show_move: { type: Boolean, default: false },
show_merge: { type: Boolean, default: false },
help: { type: String, default: undefined },
},
},
methods: {
}
data() {
return {
new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
new_value: function () {
this.$root.$emit("change", this.field, this.new_value)
},
},
methods: {},
}
</script>
</script>

View File

@ -2,22 +2,26 @@
<div>
<b-modal :id="'modal_' + id" @hidden="cancelAction">
<template v-slot:modal-title>
<h4>{{ form.title }}</h4>
<h4 class="d-inline">{{ form.title }}</h4>
<help-badge v-if="form.show_help" @show="show_help = true" @hide="show_help = false" :component="`GenericModal${form.title}`" />
</template>
<div v-for="(f, i) in form.fields" v-bind:key="i">
<p v-if="visibleCondition(f, 'instruction')">{{ f.label }}</p>
<lookup-input v-if="visibleCondition(f, 'lookup')" :form="f" :model="listModel(f.list)" @change="storeValue" />
<checkbox-input class="mb-3" v-if="visibleCondition(f, 'checkbox')" :label="f.label" :value="f.value" :field="f.field" />
<text-input v-if="visibleCondition(f, 'text')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />
<lookup-input v-if="visibleCondition(f, 'lookup')" :form="f" :model="listModel(f.list)" @change="storeValue" :help="showHelp && f.help" />
<checkbox-input class="mb-3" v-if="visibleCondition(f, 'checkbox')" :label="f.label" :value="f.value" :field="f.field" :help="showHelp && f.help" />
<text-input v-if="visibleCondition(f, 'text')" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" :help="showHelp && f.help" :subtitle="f.subtitle" />
<choice-input v-if="visibleCondition(f, 'choice')" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder" />
<emoji-input v-if="visibleCondition(f, 'emoji')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<file-input v-if="visibleCondition(f, 'file')" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
</div>
<template v-slot:modal-footer>
<b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
<div class="row w-100 justify-content-end">
<div class="col-auto">
<b-button class="mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
<b-button class="mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
</div>
</div>
</template>
</b-modal>
</div>
@ -31,7 +35,7 @@ import { getForm, formFunctions } from "@/utils/utils"
Vue.use(BootstrapVue)
import { ApiApiFactory } from "@/utils/openapi/api"
import { ApiMixin, StandardToasts, ToastMixin } from "@/utils/utils"
import { ApiMixin, StandardToasts, ToastMixin, getUserPreference } from "@/utils/utils"
import CheckboxInput from "@/components/Modals/CheckboxInput"
import LookupInput from "@/components/Modals/LookupInput"
import TextInput from "@/components/Modals/TextInput"
@ -39,10 +43,11 @@ import EmojiInput from "@/components/Modals/EmojiInput"
import ChoiceInput from "@/components/Modals/ChoiceInput"
import FileInput from "@/components/Modals/FileInput"
import SmallText from "@/components/Modals/SmallText"
import HelpBadge from "@/components/Badges/Help"
export default {
name: "GenericModalForm",
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText },
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput, SmallText, HelpBadge },
mixins: [ApiMixin, ToastMixin],
props: {
model: { required: true, type: Object },
@ -73,21 +78,30 @@ export default {
form: {},
dirty: false,
special_handling: false,
show_help: true,
}
},
mounted() {
this.id = Math.random()
this.$root.$on("change", this.storeValue) // boostrap modal placed at document so have to listen at root of component
this.$root.$on("change", this.storeValue) // bootstrap modal placed at document so have to listen at root of component
},
computed: {
buttonLabel() {
return this.buttons[this.action].label
},
showHelp() {
if (this.show_help) {
return true
} else {
return undefined
}
},
},
watch: {
show: function () {
if (this.show) {
this.form = getForm(this.model, this.action, this.item1, this.item2)
if (this.form?.form_function) {
this.form = formFunctions[this.form.form_function](this.form)
}
@ -256,15 +270,33 @@ export default {
let type_match = field?.type == field_type
let checks = true
if (type_match && field?.condition) {
if (field.condition?.condition === "exists") {
if ((this.item1[field.condition.field] != undefined) === field.condition.value) {
checks = true
} else {
checks = false
}
const value = this.item1[field?.condition?.field]
const preference = getUserPreference(field?.condition?.field)
console.log("condition", field?.condition?.condition)
switch (field?.condition?.condition) {
case "field_exists":
if ((value != undefined) === field.condition.value) {
checks = true
} else {
checks = false
}
break
case "preference__array_exists":
if (preference?.length > 0 === field.condition.value) {
checks = true
} else {
checks = false
}
break
case "preference_equals":
if (preference === field.condition.value) {
checks = true
} else {
checks = false
}
break
}
}
return type_match && checks
},
},

View File

@ -1,6 +1,6 @@
<template>
<div>
<b-form-group class="mb-3">
<b-form-group :class="class_list">
<template #label v-if="show_label">
{{ form.label }}
</template>
@ -19,6 +19,7 @@
@new="addNew"
>
</generic-multiselect>
<em v-if="help" class="small text-muted">{{ help }}</em>
</b-form-group>
</div>
</template>
@ -44,8 +45,10 @@ export default {
return undefined
},
},
class_list: { type: String, default: "mb-3" },
show_label: { type: Boolean, default: true },
clear: { type: Number },
help: { type: String, default: undefined },
},
data() {
return {
@ -70,6 +73,9 @@ export default {
return this.form?.multiple || this.form?.ordered || false
},
initialSelection() {
if (!this.new_value) {
return
}
let this_value = this.new_value
let arrayValues = undefined
// multiselect is expect to get an array of objects - make sure it gets one
@ -82,7 +88,7 @@ export default {
} else {
arrayValues = [{ id: -1, name: this_value }]
}
if (this.form?.ordered && this.first_run && arrayValues.length > 0) {
if (this.form?.ordered && this.first_run) {
return this.flattenItems(arrayValues)
} else {
return arrayValues

View File

@ -2,7 +2,7 @@
<div>
<b-modal :id="`shopping_${this.modal_id}`" hide-footer @show="loadRecipe">
<template v-slot:modal-title
><h4>{{ $t("Add_Servings_to_Shopping", { servings: servings }) }}</h4></template
><h4>{{ $t("Add_Servings_to_Shopping", { servings: recipe_servings }) }}</h4></template
>
<loading-spinner v-if="loading"></loading-spinner>
<div class="accordion" role="tablist" v-if="!loading">
@ -15,7 +15,7 @@
:steps="steps"
:recipe="recipe.id"
:ingredient_factor="ingredient_factor"
:servings="servings"
:servings="recipe_servings"
:show_shopping="true"
:add_shopping_mode="true"
:header="false"
@ -33,7 +33,7 @@
:steps="r.steps"
:recipe="r.recipe.id"
:ingredient_factor="ingredient_factor"
:servings="servings"
:servings="recipe_servings"
:show_shopping="true"
:add_shopping_mode="true"
:header="false"
@ -45,12 +45,19 @@
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
</b-card>
</div>
<div class="row mt-3 mb-3">
<div class="col-12 text-right">
<b-button class="mx-2" variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
<b-button class="mx-2" variant="success" @click="saveShopping">{{ $t("Save") }} </b-button>
</div>
</div>
<b-input-group class="my-3">
<b-input-group-prepend is-text>
{{ $t("Servings") }}
</b-input-group-prepend>
<b-form-spinbutton min="1" v-model="recipe_servings" inline style="height: 3em"></b-form-spinbutton>
<b-input-group-append>
<b-button variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
<b-button variant="success" @click="saveShopping">{{ $t("Save") }} </b-button>
</b-input-group-append>
</b-input-group>
</b-modal>
</div>
</template>
@ -71,25 +78,38 @@ export default {
mixins: [],
props: {
recipe: { required: true, type: Object },
servings: { type: Number },
servings: { type: Number, default: undefined },
modal_id: { required: true, type: Number },
},
data() {
return {
loading: true,
steps: [],
recipe_servings: 0,
recipe_servings: undefined,
add_shopping: [],
related_recipes: [],
}
},
mounted() {},
mounted() {
this.recipe_servings = this.servings
},
computed: {
ingredient_factor: function () {
return this.servings / this.recipe.servings || this.recipe_servings
return this.recipe_servings / this.recipe.servings
},
},
watch: {
recipe: {
handler() {
this.loadRecipe()
},
deep: true,
},
servings: function (newVal) {
console.log(newVal)
this.recipe_servings = parseInt(newVal)
},
},
watch: {},
methods: {
loadRecipe: function () {
this.add_shopping = []
@ -109,7 +129,9 @@ export default {
.filter((x) => !x?.food?.food_onhand)
.map((x) => x.id),
]
this.recipe_servings = result.data?.servings
if (!this.recipe_servings) {
this.recipe_servings = result.data?.servings
}
this.loading = false
})
.then(() => {
@ -159,19 +181,27 @@ export default {
let shopping_recipe = {
id: this.recipe.id,
ingredients: this.add_shopping,
servings: this.servings,
servings: this.recipe_servings,
}
let apiClient = new ApiApiFactory()
apiClient
.shoppingRecipe(this.recipe.id, shopping_recipe)
.then((result) => {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
this.$emit("finish")
})
.catch((err) => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
this.$bvModal.hide(`shopping_${this.modal_id}`)
},
},
}
</script>
<style>
.b-form-spinbutton.form-control {
background-color: #e9ecef;
border: 1px solid #ced4da;
}
</style>

View File

@ -2,6 +2,8 @@
<div>
<b-form-group v-bind:label="label" class="mb-3">
<b-form-input v-model="new_value" type="text" :placeholder="placeholder"></b-form-input>
<em v-if="help" class="small text-muted">{{ help }}</em>
<small v-if="subtitle" class="text-muted">{{ subtitle }}</small>
</b-form-group>
</div>
</template>
@ -14,7 +16,8 @@ export default {
label: { type: String, default: "Text Field" },
value: { type: String, default: "" },
placeholder: { type: String, default: "You Should Add Placeholder Text" },
show_merge: { type: Boolean, default: false },
help: { type: String, default: undefined },
subtitle: { type: String, default: undefined },
},
data() {
return {

View File

@ -26,11 +26,21 @@
</a>
<a href="javascript:void(0);">
<button class="dropdown-item" onclick="window.print()"><i class="fas fa-print fa-fw"></i> {{ $t("Print") }}</button>
<button class="dropdown-item" onclick="window.print()">
<i class="fas fa-print fa-fw"></i>
{{ $t("Print") }}
</button>
</a>
<a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-export fa-fw"></i> {{ $t("Export") }}</a>
<a href="javascript:void(0);">
<button class="dropdown-item" @click="pinRecipe()">
<i class="fas fa-thumbtack fa-fw"></i>
{{ $t("Pin") }}
</button>
</a>
<a href="javascript:void(0);">
<button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal"><i class="fas fa-share-alt fa-fw"></i> {{ $t("Share") }}</button>
</a>
@ -38,7 +48,8 @@
</div>
<cook-log :recipe="recipe" :modal_id="modal_id"></cook-log>
<add-recipe-to-book :recipe="recipe" :modal_id="modal_id"></add-recipe-to-book>
<add-recipe-to-book :recipe="recipe" :modal_id="modal_id" :entryEditing_inital_servings="servings_value"></add-recipe-to-book>
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" />
<b-modal :id="`modal-share-link_${modal_id}`" v-bind:title="$t('Share')" hide-footer>
<div class="row">
@ -46,7 +57,7 @@
<label v-if="recipe_share_link !== undefined">{{ $t("Public share link") }}</label>
<input ref="share_link_ref" class="form-control" v-model="recipe_share_link" />
<b-button class="mt-2 mb-3 d-none d-md-inline" variant="secondary" @click="$bvModal.hide(`modal-share-link_${modal_id}`)">{{ $t("Close") }} </b-button>
<b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{ $t("Copy") }}</b-button>
<b-button class="mt-2 mb-3 ml-md-2" variant="primary" @click="copyShareLink()">{{ $t("Copy") }} </b-button>
<b-button class="mt-2 mb-3 ml-2 float-right" variant="success" @click="shareIntend()">{{ $t("Share") }} <i class="fa fa-share-alt"></i></b-button>
</div>
</div>
@ -55,14 +66,13 @@
<meal-plan-edit-modal
:entry="entryEditing"
:entryEditing_initial_recipe="[recipe]"
:entryEditing_inital_servings="recipe.servings"
:entryEditing_inital_servings="servings_value"
:entry-editing_initial_meal_type="[]"
@save-entry="saveMealPlan"
:modal_id="`modal-meal-plan_${modal_id}`"
:allow_delete="false"
:modal_title="$t('Create_Meal_Plan_Entry')"
></meal-plan-edit-modal>
<shopping-modal :recipe="recipe" :servings="servings_value" :modal_id="modal_id" />
</div>
</template>
@ -120,7 +130,21 @@ export default {
mounted() {
this.servings_value = this.servings === -1 ? this.recipe.servings : this.servings
},
watch: {
recipe: {
handler() {},
deep: true,
},
servings: function (newVal) {
this.servings_value = parseInt(newVal)
},
},
methods: {
pinRecipe: function () {
let pinnedRecipes = JSON.parse(localStorage.getItem("pinned_recipes")) || []
pinnedRecipes.push({ id: this.recipe.id, name: this.recipe.name })
localStorage.setItem("pinned_recipes", JSON.stringify(pinnedRecipes))
},
saveMealPlan: function (entry) {
entry.date = moment(entry.date).format("YYYY-MM-DD")

View File

@ -1,287 +1,334 @@
<template>
<div id="shopping_line_item">
<div class="col-12">
<b-container fluid>
<!-- summary rows -->
<b-row align-h="start">
<b-col cols="12" sm="2">
<div style="position: static" class="btn-group">
<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-body pr-1 dropdown-toggle-no-caret"
@click.stop="$emit('open-context-menu', $event, entries)"
>
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
</div>
<input type="checkbox" class="text-right mx-3 mt-2" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</div>
</b-col>
<b-col cols="12" sm="10">
<b-row>
<b-col cols="6" sm="3">
<div v-if="Object.entries(formatAmount).length == 1">{{ Object.entries(formatAmount)[0][1] }} &ensp; {{ Object.entries(formatAmount)[0][0] }}</div>
<div class="small" v-else v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }} &ensp; {{ x[0] }}</div>
</b-col>
<b-col cols="6" sm="7">
{{ formatFood }}
</b-col>
<b-col cols="6" sm="2" data-html2canvas-ignore="true">
<b-button size="sm" @click="showDetails = !showDetails" class="mr-2" variant="link">
<div class="text-nowrap">{{ showDetails ? "Hide" : "Show" }} Details</div>
</b-button>
</b-col>
</b-row>
</b-col>
</b-row>
<b-row align-h="center">
<b-col cols="12">
<div class="small text-muted text-truncate">{{ formatHint }}</div>
</b-col>
</b-row>
</b-container>
<!-- detail rows -->
<div class="card no-body" v-if="showDetails">
<b-container fluid>
<div v-for="e in entries" :key="e.id">
<b-row class="ml-2 small">
<b-col cols="6" md="4" class="overflow-hidden text-nowrap">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn btn-link btn-sm m-0 p-0"
style="text-overflow: ellipsis"
@click.stop="openRecipeCard($event, e)"
@mouseover="openRecipeCard($event, e)"
>
{{ formatOneRecipe(e) }}
</button>
</b-col>
<b-col cols="6" md="4" class="col-md-4 text-muted">{{ formatOneMealPlan(e) }}</b-col>
<b-col cols="12" md="4" class="col-md-4 text-muted text-right overflow-hidden text-nowrap">
{{ formatOneCreatedBy(e) }}
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
</b-col>
</b-row>
<b-row class="ml-2 light">
<b-col cols="12" sm="2">
<div style="position: static" class="btn-group">
<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-body pr-1 dropdown-toggle-no-caret"
@click.stop="$emit('open-context-menu', $event, e)"
>
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
</div>
<input type="checkbox" class="text-right mx-3 mt-2" :checked="e.checked" @change="updateChecked($event, e)" />
</div>
</b-col>
<b-col cols="12" sm="10">
<b-row>
<b-col cols="2" sm="2" md="1" class="text-nowrap">{{ formatOneAmount(e) }}</b-col>
<b-col cols="10" sm="4" md="2" class="text-nowrap">{{ formatOneUnit(e) }}</b-col>
<b-col cols="12" sm="6" md="4" class="text-nowrap">{{ formatOneFood(e) }}</b-col>
<b-col cols="12" sm="6" md="5">
<div class="small" v-for="(n, i) in formatOneNote(e)" :key="i">{{ n }}</div>
</b-col>
</b-row>
</b-col>
</b-row>
<hr class="w-75" />
</div>
</b-container>
</div>
<hr class="m-1" />
<div id="shopping_line_item">
<b-row align-h="start">
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
v-if="settings.left_handed">
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
</div>
</b-button>
</b-col>
<b-col cols="1" class="align-items-center d-flex">
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
@click.stop="$emit('open-context-menu', $event, entries)">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
:class="settings.left_handed ? 'dropdown-spacing' : ''"
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret">
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
</div>
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
<template #menu="{ contextData }" v-if="recipe">
<ContextMenuItem><RecipeCard :recipe="contextData" :detail="false"></RecipeCard></ContextMenuItem>
<ContextMenuItem @click="$refs.menu.close()">
<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="fas fa-pizza-slice"></i> {{ $t("Servings") }}</a>
</template>
<div @click.prevent.stop>
<b-form-input class="mt-2" min="0" type="number" v-model="servings"></b-form-input>
</div>
</b-form-group>
</ContextMenuItem>
</template>
</ContextMenu>
</b-col>
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
<b-col cols="8" md="9">
<b-row class="d-flex h-100">
<b-col cols="5" md="3" class="d-flex align-items-center" v-if="Object.entries(formatAmount).length == 1">
<strong class="mr-1">{{ Object.entries(formatAmount)[0][1] }}</strong> {{
Object.entries(formatAmount)[0][0]
}}
</b-col>
<b-col cols="5" md="3" class="d-flex flex-column" v-if="Object.entries(formatAmount).length != 1">
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }} &ensp;
{{ x[0] }}
</div>
</b-col>
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
{{ formatFood }}
</b-col>
<b-col cols="3" data-html2canvas-ignore="true"
class="align-items-center d-none d-md-flex justify-content-end">
<b-button size="sm" @click="showDetails = !showDetails" class="p-0 mr-0 mr-md-2 p-md-2 text-decoration-none"
variant="link">
<div class="text-nowrap"><i class="fa fa-chevron-right rotate"
:class="showDetails ? 'rotated' : ''"></i> <span
class="d-none d-md-inline-block">{{ $t('Details') }}</span>
</div>
</b-button>
</b-col>
</b-row>
</b-col>
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none"
v-if="!settings.left_handed">
<b-button size="sm" @click="showDetails = !showDetails" class="d-inline-block d-md-none p-0" variant="link">
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
</div>
</b-button>
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
</b-row>
<b-row align-h="center" class="d-none d-md-flex">
<b-col cols="12">
<div class="small text-muted text-truncate">{{ formatHint }}</div>
</b-col>
</b-row>
<!-- detail rows -->
<div class="card no-body mb-1 pt-2 align-content-center shadow-sm" v-if="showDetails">
<div v-for="(e, x) in entries" :key="e.id">
<b-row class="small justify-content-around">
<b-col cols="auto" md="4" class="overflow-hidden text-nowrap">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn btn-link btn-sm m-0 p-0 pl-2"
style="text-overflow: ellipsis"
@click.stop="openRecipeCard($event, e)"
@mouseover="openRecipeCard($event, e)">
{{ formatOneRecipe(e) }}
</button>
</b-col>
<b-col cols="auto" md="4" class="text-muted">{{ formatOneMealPlan(e) }}</b-col>
<b-col cols="auto" md="4" class="text-muted text-right overflow-hidden text-nowrap pr-4">
{{ formatOneCreatedBy(e) }}
<div v-if="formatOneCompletedAt(e)">{{ formatOneCompletedAt(e) }}</div>
</b-col>
</b-row>
<b-row align-h="start">
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0"
v-if="settings.left_handed">
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
:checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
<b-col cols="1" class="align-items-center d-flex">
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true"
@click.stop="$emit('open-context-menu', $event, e)">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
:class="settings.left_handed ? 'dropdown-spacing' : ''"
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret">
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
</div>
</b-col>
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
<b-col cols="8" md="9">
<b-row class="d-flex align-items-center h-100">
<b-col cols="5" md="3" class="d-flex align-items-center">
<strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }}
</b-col>
<b-col cols="7" md="6" class="align-items-center d-flex pl-0 pr-0 pl-md-2 pr-md-2">
{{ formatOneFood(e) }}
</b-col>
<b-col cols="12" class="d-flex d-md-none">
<div class="small text-muted text-truncate" v-for="(n, i) in formatOneNote(e)" :key="i">{{ n }}</div>
</b-col>
</b-row>
</b-col>
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none"
v-if="!settings.left_handed">
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile"
:checked="formatChecked"
@change="updateChecked"
:key="entries[0].id"/>
</b-col>
</b-row>
<hr class="w-75" v-if="x !== entries.length -1"/>
<div class="pb-4" v-if="x === entries.length -1"></div>
</div>
</div>
<hr class="m-1" v-if="!showDetails"/>
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
<template #menu="{ contextData }" v-if="recipe">
<ContextMenuItem>
<RecipeCard :recipe="contextData" :detail="false"></RecipeCard>
</ContextMenuItem>
<ContextMenuItem @click="$refs.menu.close()">
<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="fas fa-pizza-slice"></i> {{ $t("Servings") }}</a>
</template>
<div @click.prevent.stop>
<b-form-input class="mt-2" min="0" type="number" v-model="servings"></b-form-input>
</div>
</b-form-group>
</ContextMenuItem>
</template>
</ContextMenu>
</div>
</template>
<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"
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
import { ApiMixin } from "@/utils/utils"
import {ApiMixin} from "@/utils/utils"
import RecipeCard from "./RecipeCard.vue"
Vue.use(BootstrapVue)
export default {
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: "ShoppingLineItem",
mixins: [ApiMixin],
components: { RecipeCard, ContextMenu, ContextMenuItem },
props: {
entries: {
type: Array,
},
groupby: { type: String },
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: "ShoppingLineItem",
mixins: [ApiMixin],
components: {RecipeCard, ContextMenu, ContextMenuItem},
props: {
entries: {
type: Array,
},
data() {
return {
showDetails: false,
recipe: undefined,
servings: 1,
settings: Object,
groupby: {type: String},
},
data() {
return {
showDetails: false,
recipe: undefined,
servings: 1,
}
},
computed: {
formatAmount: function () {
let amount = {}
this.entries.forEach((entry) => {
let unit = entry?.unit?.name ?? "----"
if (entry.amount) {
if (amount[unit]) {
amount[unit] += entry.amount
} else {
amount[unit] = entry.amount
}
}
})
for (const [k, v] of Object.entries(amount)) {
amount[k] = Math.round(v * 100 + Number.EPSILON) / 100 // javascript hack to force rounding at 2 places
}
return amount
},
computed: {
formatAmount: function () {
let amount = {}
this.entries.forEach((entry) => {
let unit = entry?.unit?.name ?? "----"
if (entry.amount) {
if (amount[unit]) {
amount[unit] += entry.amount
} else {
amount[unit] = entry.amount
}
}
})
for (const [k, v] of Object.entries(amount)) {
amount[k] = Math.round(v * 100 + Number.EPSILON) / 100 // javascript hack to force rounding at 2 places
}
return amount
},
formatCategory: function () {
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
},
formatChecked: function () {
return this.entries.map((x) => x.checked).every((x) => x === true)
},
formatHint: function () {
if (this.groupby == "recipe") {
return this.formatCategory
} else {
return this.formatRecipe
}
},
formatFood: function () {
return this.formatOneFood(this.entries[0])
},
formatUnit: function () {
return this.formatOneUnit(this.entries[0])
},
formatRecipe: function () {
if (this.entries?.length == 1) {
return this.formatOneMealPlan(this.entries[0]) || ""
} else {
let mealplan_name = this.entries.filter((x) => x?.recipe_mealplan?.name)
// return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
formatCategory: function () {
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
},
formatChecked: function () {
return this.entries.map((x) => x.checked).every((x) => x === true)
},
formatHint: function () {
if (this.groupby == "recipe") {
return this.formatCategory
} else {
return this.formatRecipe
}
},
formatFood: function () {
return this.formatOneFood(this.entries[0])
},
formatUnit: function () {
return this.formatOneUnit(this.entries[0])
},
formatRecipe: function () {
if (this.entries?.length == 1) {
return this.formatOneMealPlan(this.entries[0]) || ""
} else {
let mealplan_name = this.entries.filter((x) => x?.recipe_mealplan?.name)
// return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
return mealplan_name
.map((x) => {
return this.formatOneMealPlan(x)
})
.join(" - ")
}
},
formatNotes: function () {
if (this.entries?.length == 1) {
return this.formatOneNote(this.entries[0]) || ""
}
return ""
},
},
watch: {},
mounted() {
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
},
methods: {
// this.genericAPI inherited from ApiMixin
formatDate: function (datetime) {
if (!datetime) {
return
}
return Intl.DateTimeFormat(window.navigator.language, { dateStyle: "short", timeStyle: "short" }).format(Date.parse(datetime))
},
formatOneAmount: function (item) {
return item?.amount ?? 1
},
formatOneUnit: function (item) {
return item?.unit?.name ?? ""
},
formatOneCategory: function (item) {
return item?.food?.supermarket_category?.name
},
formatOneCompletedAt: function (item) {
if (!item.completed_at) {
return false
}
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
},
formatOneFood: function (item) {
return item.food.name
},
formatOneDelayUntil: function (item) {
if (!item.delay_until || (item.delay_until && item.checked)) {
return false
}
return [this.$t("DelayUntil"), "-", this.formatDate(item.delay_until)].join(" ")
},
formatOneMealPlan: function (item) {
return item?.recipe_mealplan?.name ?? ""
},
formatOneRecipe: function (item) {
return item?.recipe_mealplan?.recipe_name ?? ""
},
formatOneNote: function (item) {
if (!item) {
item = this.entries[0]
}
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
},
formatOneCreatedBy: function (item) {
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
},
openRecipeCard: function (e, item) {
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, { id: item.recipe_mealplan.recipe }).then((result) => {
let recipe = result.data
recipe.steps = undefined
this.recipe = true
this.$refs.recipe_card.open(e, recipe)
return mealplan_name
.map((x) => {
return this.formatOneMealPlan(x)
})
},
updateChecked: function (e, item) {
let update = undefined
if (!item) {
update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked }
} else {
update = { entries: [item], checked: !item.checked }
}
this.$emit("update-checkbox", update)
},
.join(" - ")
}
},
formatNotes: function () {
if (this.entries?.length == 1) {
return this.formatOneNote(this.entries[0]) || ""
}
return ""
},
},
watch: {},
mounted() {
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
},
methods: {
// this.genericAPI inherited from ApiMixin
formatDate: function (datetime) {
if (!datetime) {
return
}
return Intl.DateTimeFormat(window.navigator.language, {
dateStyle: "short",
timeStyle: "short"
}).format(Date.parse(datetime))
},
formatOneAmount: function (item) {
return item?.amount ?? 1
},
formatOneUnit: function (item) {
return item?.unit?.name ?? ""
},
formatOneCategory: function (item) {
return item?.food?.supermarket_category?.name
},
formatOneCompletedAt: function (item) {
if (!item.completed_at) {
return false
}
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
},
formatOneFood: function (item) {
return item.food.name
},
formatOneDelayUntil: function (item) {
if (!item.delay_until || (item.delay_until && item.checked)) {
return false
}
return [this.$t("DelayUntil"), "-", this.formatDate(item.delay_until)].join(" ")
},
formatOneMealPlan: function (item) {
return item?.recipe_mealplan?.name ?? ""
},
formatOneRecipe: function (item) {
return item?.recipe_mealplan?.recipe_name ?? ""
},
formatOneNote: function (item) {
if (!item) {
item = this.entries[0]
}
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
},
formatOneCreatedBy: function (item) {
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
},
openRecipeCard: function (e, item) {
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, {id: item.recipe_mealplan.recipe}).then((result) => {
let recipe = result.data
recipe.steps = undefined
this.recipe = true
this.$refs.recipe_card.open(e, recipe)
})
},
updateChecked: function (e, item) {
let update = undefined
if (!item) {
update = {entries: this.entries.map((x) => x.id), checked: !this.formatChecked}
} else {
update = {entries: [item], checked: !item.checked}
}
this.$emit("update-checkbox", update)
},
},
}
</script>
@ -296,4 +343,35 @@ export default {
/* left: 0; top: 50%; width: 100%; /* …with the top across the middle */
/* border-bottom: 1px solid #000; /* …and with a border on the top */
/* } */
.checkbox-control {
font-size: 0.6rem
}
.checkbox-control-mobile {
font-size: 1rem
}
.rotate {
-moz-transition: all 0.25s linear;
-webkit-transition: all 0.25s linear;
transition: all 0.25s linear;
}
.rotated {
-moz-transform: rotate(90deg);
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.unit-badge-lg {
font-size: 1rem !important;
font-weight: 500 !important;
}
@media (max-width: 768px) {
.dropdown-spacing {
padding-left: 0 !important;
padding-right: 0 !important;
}
}
</style>

View File

@ -1,126 +1,118 @@
<template>
<div>
<hr />
<template v-if="step.type === 'TEXT' || step.type === 'RECIPE'">
<div class="row" v-if="recipe.steps.length > 1">
<div class="col col-md-8">
<h5 class="text-primary">
<template v-if="step.name">{{ step.name }}</template>
<template v-else>{{ $t("Step") }} {{ index + 1 }}</template>
<small style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fas fa-user-clock"></i> {{ step.time }} {{ $t("min") }} </small>
<small v-if="start_time !== ''" class="d-print-none">
<b-link :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#">
{{ moment(start_time).add(step.time_offset, "minutes").format("HH:mm") }}
</b-link>
</small>
</h5>
</div>
<div class="col col-md-4" style="text-align: right">
<b-button
@click="details_visible = !details_visible"
style="border: none; background: none"
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible }"
>
<i class="far fa-check-circle"></i>
</b-button>
</div>
<hr/>
<!-- Step header (only shown if more than one step -->
<div class="row mb-1" v-if="recipe.steps.length > 1">
<div class="col col-md-8">
<h5 class="text-primary">
<template v-if="step.name">{{ step.name }}</template>
<template v-else>{{ $t("Step") }} {{ index + 1 }}</template>
<small style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i
class="fas fa-user-clock"></i> {{ step.time }} {{ $t("min") }} </small>
<small v-if="start_time !== ''" class="d-print-none">
<b-link :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#">
{{ moment(start_time).add(step.time_offset, "minutes").format("HH:mm") }}
</b-link>
</small>
</h5>
</div>
</template>
<div class="col col-md-4 text-right">
<b-button
@click="details_visible = !details_visible"
style="border: none; background: none"
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible }"
>
<i class="far fa-check-circle"></i>
</b-button>
</div>
</div>
<template v-if="step.type === 'TEXT'">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="row">
<div class="col col-md-4" v-if="step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
<table class="table table-sm">
<ingredients-card :steps="[step]" :ingredient_factor="ingredient_factor" @checked-state-changed="$emit('checked-state-changed', $event)" />
</table>
</div>
<div class="col" :class="{ 'col-md-8': recipe.steps.length > 1, 'col-md-12': recipe.steps.length <= 1 }">
<compile-component :code="step.ingredients_markdown" :ingredient_factor="ingredient_factor"></compile-component>
</div>
</div>
</b-collapse>
</template>
<b-collapse id="collapse-1" v-model="details_visible">
<template v-if="step.type === 'TIME' || step.type === 'FILE'">
<div class="row">
<div class="col-md-8 offset-md-2" style="text-align: center">
<h4 class="text-primary">
<template v-if="step.name">{{ step.name }}</template>
<template v-else>{{ $t("Step") }} {{ index + 1 }}</template>
</h4>
<span style="margin-left: 4px" class="text-muted" v-if="step.time !== 0"><i class="fa fa-stopwatch"></i> {{ step.time }} {{ $t("min") }}</span>
<b-link class="d-print-none" :id="`id_reactive_popover_${step.id}`" @click="openPopover" href="#" v-if="start_time !== ''">
{{ moment(start_time).add(step.time_offset, "minutes").format("HH:mm") }}
</b-link>
<!-- ingredients table -->
<div class="col col-md-4" v-if="step.ingredients.length > 0 && (recipe.steps.length > 1 || force_ingredients)">
<table class="table table-sm" >
<ingredients-card :steps="[step]" :ingredient_factor="ingredient_factor"
@checked-state-changed="$emit('checked-state-changed', $event)"/>
</table>
</div>
<div class="col-md-2" style="text-align: right">
<b-button
@click="details_visible = !details_visible"
style="border: none; background: none"
class="shadow-none d-print-none"
:class="{ 'text-primary': details_visible, 'text-success': !details_visible }"
>
<i class="far fa-check-circle"></i>
</b-button>
<div class="col"
:class="{ 'col-md-8': recipe.steps.length > 1, 'col-md-12': recipe.steps.length <= 1 }">
<!-- step text -->
<div class="row">
<div class="col col-md-12">
<compile-component :code="step.ingredients_markdown"
:ingredient_factor="ingredient_factor"></compile-component>
</div>
</div>
<!-- File (preview if image, download else) -->
<div class="row" v-if="step.file !== null">
<div class="col col-md-12">
<template>
<div
v-if="step.file.file.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
<b-img :src="step.file.file" fluid-grow></b-img>
</div>
<div v-else>
<a :href="step.file.file" target="_blank"
rel="noreferrer nofollow">{{ $t("Download") }}
{{ $t("File") }}</a>
</div>
</template>
</div>
</div>
</div>
</div>
<b-collapse id="collapse-1" v-model="details_visible">
<div class="row" v-if="step.instruction !== ''">
<div class="col col-md-12" style="text-align: center">
<compile-component :code="step.ingredients_markdown" :ingredient_factor="ingredient_factor"></compile-component>
<!-- Sub recipe (always full width own row) -->
<div class="row">
<div class="col col-md-12">
<div class="card" v-if="step.step_recipe_data !== null">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="card-body">
<h2 class="card-title">
<a :href="resolveDjangoUrl('view_recipe', step.step_recipe_data.id)">{{
step.step_recipe_data.name
}}</a>
</h2>
<div v-for="(sub_step, index) in step.step_recipe_data.steps"
v-bind:key="`substep_${sub_step.id}`">
<step-component
:recipe="step.step_recipe_data"
:step="sub_step"
:ingredient_factor="ingredient_factor"
:index="index"
:start_time="start_time"
:force_ingredients="true"
></step-component>
</div>
</div>
</b-collapse>
</div>
</div>
</b-collapse>
</template>
<div class="row" style="text-align: center">
<div class="col col-md-12">
<template v-if="step.file !== null">
<div v-if="step.file.file.includes('.png') || recipe.file_path.includes('.jpg') || recipe.file_path.includes('.jpeg') || recipe.file_path.includes('.gif')">
<img :src="step.file.file" style="max-width: 50vw; max-height: 50vh" />
</div>
<div v-else>
<a :href="step.file.file" target="_blank" rel="noreferrer nofollow">{{ $t("Download") }} {{ $t("File") }}</a>
</div>
</template>
</div>
</div>
<div class="card" v-if="step.type === 'RECIPE' && step.step_recipe_data !== null">
<b-collapse id="collapse-1" v-model="details_visible">
<div class="card-body">
<h2 class="card-title">
<a :href="resolveDjangoUrl('view_recipe', step.step_recipe_data.id)">{{ step.step_recipe_data.name }}</a>
</h2>
<div v-for="(sub_step, index) in step.step_recipe_data.steps" v-bind:key="`substep_${sub_step.id}`">
<step-component
:recipe="step.step_recipe_data"
:step="sub_step"
:ingredient_factor="ingredient_factor"
:index="index"
:start_time="start_time"
:force_ingredients="true"
></step-component>
</div>
</div>
</b-collapse>
</div>
</b-collapse>
<!-- Time popover (not rendered) -->
<div v-if="start_time !== ''">
<b-popover :target="`id_reactive_popover_${step.id}`" triggers="click" placement="bottom" :ref="`id_reactive_popover_${step.id}`" :title="$t('Step start time')">
<b-popover :target="`id_reactive_popover_${step.id}`" triggers="click" placement="bottom"
:ref="`id_reactive_popover_${step.id}`" :title="$t('Step start time')">
<div>
<b-form-group label="Time" label-for="popover-input-1" label-cols="3" class="mb-1">
<b-form-input type="datetime-local" id="popover-input-1" v-model.datetime-local="set_time_input" size="sm"></b-form-input>
<b-form-input type="datetime-local" id="popover-input-1" v-model.datetime-local="set_time_input"
size="sm"></b-form-input>
</b-form-group>
</div>
<div class="row" style="margin-top: 1vh">
<div class="col-12" style="text-align: right">
<b-button @click="closePopover" size="sm" variant="secondary" style="margin-right: 8px">{{ $t("Cancel") }}</b-button>
<b-button @click="closePopover" size="sm" variant="secondary" style="margin-right: 8px">
{{ $t("Cancel") }}
</b-button>
<b-button @click="updateTime" size="sm" variant="primary">{{ $t("Ok") }}</b-button>
</div>
</div>
@ -130,22 +122,22 @@
</template>
<script>
import { calculateAmount } from "@/utils/utils"
import {calculateAmount} from "@/utils/utils"
import { GettextMixin } from "@/utils/utils"
import {GettextMixin} from "@/utils/utils"
import CompileComponent from "@/components/CompileComponent"
import IngredientsCard from "@/components/IngredientsCard"
import Vue from "vue"
import moment from "moment"
import { ResolveUrlMixin } from "@/utils/utils"
import {ResolveUrlMixin} from "@/utils/utils"
Vue.prototype.moment = moment
export default {
name: "StepComponent",
mixins: [GettextMixin, ResolveUrlMixin],
components: { CompileComponent, IngredientsCard },
components: {CompileComponent, IngredientsCard},
props: {
step: Object,
ingredient_factor: Number,

View File

@ -19,5 +19,6 @@ function loadLocaleMessages () {
export default new VueI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
silentTranslationWarn: true, //TODO fallback war seems to not work, try in next version https://kazupon.github.io/vue-i18n/api/#constructor-options
messages: loadLocaleMessages()
})

View File

@ -208,5 +208,89 @@
"Coming_Soon": "Bald verfügbar",
"Auto_Planner": "Smart Planen",
"Hide_Keyword": "Keywords schließen",
"Clear": "Leeren"
"Clear": "Leeren",
"GroupBy": "Gruppieren nach",
"IgnoreThis": "{food} nicht automatisch zur Einkaufsliste hinzufügen",
"shopping_auto_sync": "Automatische Synchronisierung",
"shopping_share_desc": "Benutzer sehen all Einträge, die du zur Einkaufsliste hinzufügst. Sie müssen dich hinzufügen, damit du Ihre Einträge sehen kannst.",
"IgnoredFood": "{food} beim nächsten Einkauf ignorieren.",
"Add_Servings_to_Shopping": "{servings} Portionen zum Einkauf hinzufügen",
"Inherit": "Vererben",
"InheritFields": "Feldwerte vererben",
"ShowUncategorizedFood": "Zeige nicht zugeordnete",
"DelayFor": "Um {hours } verschieben",
"Warning": "Warnung",
"NoCategory": "Keine Kategorie ausgewählt.",
"ShowDelayed": "Zeige verschobene Elemente",
"Completed": "Vollständig",
"OfflineAlert": "Du bist offline, deine Einkaufsliste wird nicht synchronisiert.",
"shopping_share": "Teile die Einkaufsliste",
"mealplan_autoadd_shopping": "Automatisches Hinzufügen zum Essensplan",
"mealplan_autoexclude_onhand": "Ignoriere vorhandene Zutaten",
"mealplan_autoinclude_related": "Füge verwandte Rezepte hinzu",
"default_delay": "Standard Zeit des Verzögerns",
"Added_by": "Hinzugefügt von",
"AddToShopping": "Zur Einkaufsliste hinzufügen",
"FoodOnHand": "Sie haben {food} vorrätig.",
"DeleteShoppingConfirm": "Möchten Sie wirklich alle {food} von der Einkaufsliste zu entfernen?",
"err_moving_resource": "Während des Verschiebens einer Resource ist ein Fehler aufgetreten!",
"err_merging_resource": "Beim Zusammenführen einer Ressource ist ein Fehler aufgetreten!",
"success_moving_resource": "Ressource wurde erfolgreich verschoben!",
"success_merging_resource": "Ressource wurde erfolgreich zusammengeführt!",
"Shopping_Categories": "Einkaufskategorien",
"Added_on": "Hinzugefügt am",
"IngredientInShopping": "Diese Zutat befindet sich in Ihrer Einkaufsliste.",
"NotInShopping": "{food} ist nicht in Ihrer Einkaufsliste.",
"OnHand": "Aktuell vorrätig",
"FoodNotOnHand": "Sie haben kein {food} vorrätig.",
"Undefined": "nicht definiert",
"AddFoodToShopping": "{food} zur Einkaufsliste hinzufügen",
"RemoveFoodFromShopping": "{food} von der Einkaufsliste entfernen",
"Search Settings": "Sucheinstellungen",
"shopping_auto_sync_desc": "Bei 0 wird Auto-Sync deaktiviert. Beim Betrachten einer Einkaufsliste wird die Liste alle gesetzten Sekunden aktualisiert, um mögliche Änderungen anderer zu zeigen. Nützlich, wenn mehrere Personen einkaufen und mobile Daten nutzen.",
"MoveCategory": "Verschieben nach: ",
"mealplan_autoadd_shopping_desc": "Essensplan-Zutaten automatisch zur Einkaufsliste hinzufügen.",
"Pin": "Pin",
"mark_complete": "Vollständig markieren",
"shopping_add_onhand_desc": "Markiere Lebensmittel als \"Vorrätig\", wenn von der Einkaufsliste abgehakt wurden.",
"left_handed": "Linkshändermodus",
"left_handed_help": "Optimiert die Benutzeroberfläche für die Bedienung mit der linken Hand.",
"FoodInherit": "Lebensmittel vererbbare Felder",
"SupermarketCategoriesOnly": "Nur Supermarkt Kategorien",
"InheritWarning": "{food} ist auf Vererbung gesetzt ist, Änderungen werden möglicherweise nicht gespeichert.",
"mealplan_autoexclude_onhand_desc": "Wenn Sie einen Essensplan zur Einkaufsliste hinzufügen (manuell oder automatisch), schließen Sie Zutaten aus, die gerade vorrätig sind.",
"mealplan_autoinclude_related_desc": "Wenn Sie einen Essensplan zur Einkaufsliste hinzufügen (manuell oder automatisch), fügen Sie alle zugehörigen Rezepte hinzu.",
"default_delay_desc": "Voreingestellte Anzahl von Stunden für die Verzögerung eines Einkaufslisteneintrags.",
"filter_to_supermarket": "Auf Supermarkt filtern",
"err_move_self": "Element kann nicht auf sich selbst verschoben werden",
"nothing": "Nichts zu tun",
"err_merge_self": "Element kann nicht mit sich selbst zusammengeführt werden",
"show_sql": "SQL anzeigen",
"filter_to_supermarket_desc": "Standardmäßig wird die Einkaufsliste so gefiltert, dass sie nur Kategorien für den ausgewählten Supermarkt enthält.",
"CategoryName": "Kategorie Name",
"SupermarketName": "Supermarkt Name",
"CategoryInstruction": "Ziehen Sie Kategorien, um die Reihenfolge zu ändern, in der die Kategorien in der Einkaufsliste erscheinen.",
"shopping_recent_days_desc": "Tage der letzten Einträge in der Einkaufsliste, die angezeigt werden sollen.",
"shopping_recent_days": "Letzte Tage",
"create_shopping_new": "Zur NEUEN Einkaufsliste hinzufügen",
"download_pdf": "PDF herunterladen",
"download_csv": "CSV herunterladen",
"csv_delim_help": "Trennzeichen für CSV-Exporte.",
"csv_delim_label": "CSV-Trennzeichen",
"SuccessClipboard": "Einkaufsliste wurde in die Zwischenablage kopiert",
"copy_to_clipboard": "In die Zwischenablage kopieren",
"csv_prefix_help": "Präfix, das beim Kopieren der Liste in die Zwischenablage hinzugefügt wird.",
"csv_prefix_label": "Listenpräfix",
"copy_markdown_table": "Als Markdown-Tabelle kopieren",
"in_shopping": "In Einkaufsliste",
"DelayUntil": "Verzögerung bis",
"QuickEntry": "Schnelleinstieg",
"shopping_add_onhand": "Automatisch vorrätig",
"related_recipes": "Ähnliche Rezepte",
"today_recipes": "Rezepte des Tages",
"sql_debug": "SQL Debug",
"remember_search": "Suchbegriff merken",
"remember_hours": "Stunden zu erinnern",
"tree_select": "Baum-Auswahl verwenden",
"CountMore": "...+{count} weitere"
}

View File

@ -116,7 +116,7 @@
"Information": "Information",
"Download": "Download",
"Create": "Create",
"Advanced Search Settings": "Advanced Search Settings",
"Search Settings": "Search Settings",
"View": "View",
"Recipes": "Recipes",
"Move": "Move",
@ -277,10 +277,22 @@
"copy_markdown_table": "Copy as Markdown Table",
"in_shopping": "In Shopping List",
"DelayUntil": "Delay Until",
"Pin": "Pin",
"mark_complete": "Mark Complete",
"QuickEntry": "Quick Entry",
"shopping_add_onhand_desc": "Mark food 'On Hand' when checked off shopping list.",
"shopping_add_onhand": "Auto On Hand",
"related_recipes": "Related Recipes",
"today_recipes": "Today's Recipes"
"today_recipes": "Today's Recipes",
"sql_debug": "SQL Debug",
"remember_search": "Remember Search",
"remember_hours": "Hours to Remember",
"tree_select": "Use Tree Selection",
"left_handed": "Left-handed mode",
"left_handed_help": "Will optimize the UI for use with your left hand.",
"OnHand_help": "Food is in inventory and will not be automatically added to a shopping list.",
"ignore_shopping_help": "Never add food to the shopping list (e.g. water)",
"shopping_category_help": "Supermarkets can be ordered and filtered by Shopping Category according to the layout of the aisles.",
"food_recipe_help": "Linking a recipe here will include the linked recipe in any other recipe that use this food",
"Foods":"Foods"
}

View File

@ -37,7 +37,7 @@
"Carbohydrates": "Glucides",
"Calories": "Calories",
"Energy": "Energie",
"Nutrition": "Informations nutritionnelles",
"Nutrition": "Valeurs nutritionnelles",
"Date": "Date",
"Share": "Partager",
"Export": "Exporter",
@ -121,7 +121,7 @@
"del_confirmation_tree": "Êtes-vous sûr de vouloir supprimer {source} et tous ses enfants ?",
"warning_feature_beta": "Cette fonctionnalité est actuellement en phase BETA (test). Veuillez vous attendre à des bugs et éventuellement à des changements avenir (éventuellement la perte de données liées aux fonctionnalités) lorsque vous utilisez cette fonctionnalité.",
"confirm_delete": "Voulez-vous vraiment supprimer {objet} ?",
"Note": "Noter",
"Note": "Notes",
"Add_Step": "Ajouter une étape",
"Step_Name": "Nom de l'étape",
"Parameter": "Paramètre",
@ -192,5 +192,96 @@
"Edit_Recipe": "Modifier une Recette",
"Move_Up": "Monter",
"Time": "Temps",
"Coming_Soon": "Bientôt disponible"
"Coming_Soon": "Bientôt disponible",
"Create_New_Shopping Category": "Ajouter une catégorie de courses",
"success_moving_resource": "Ressource correctement déplacée !",
"err_moving_resource": "Il y a eu une erreur pour déplacer une ressource !",
"err_merging_resource": "Il y a eu une erreur pour fusionner une ressource !",
"success_merging_resource": "Ressource correctement fusionnée !",
"Added_by": "Ajouter par",
"Added_on": "Ajouter le",
"Shopping_Categories": "Catégories de courses",
"Add_Servings_to_Shopping": "Ajouter {servings} partions aux courses",
"CountMore": "...+ {count} en plus",
"NoCategory": "Pas de catégorie sélectionnée.",
"OfflineAlert": "Vous êtes déconnecté, votre liste de courses peut ne pas être synchronisée.",
"shopping_share_desc": "Les utilisateurs verront tous les articles que vous ajoutez à votre liste de courses. Ils doivent vous ajouter pour que vous voyez les articles de leur liste.",
"shopping_auto_sync_desc": "Le réglage sur 0 désactive la synchronisation automatique. Lorsque vous consultez une liste de courses, celle-ci est mise à jour toutes les secondes pour synchroniser les modifications apportées par une autre personne. Cette fonction est utile lorsque vous faites des achats avec plusieurs personnes, mais elle consomme des données mobiles.",
"mealplan_autoinclude_related_desc": "Lorsque vous ajoutez un plan de repas à la liste de courses (manuellement ou automatiquement), incluez toutes les recettes associées.",
"err_move_self": "Impossible de déplacer un élément vers lui-même",
"show_sql": "Montrer le SQL",
"filter_to_supermarket_desc": "Par défaut, la liste de courses est filtrée pour n'inclure que les catégories du supermarché sélectionné.",
"CategoryInstruction": "Faites glisser les catégories pour modifier l'ordre dans lequel elles apparaissent dans la liste des courses.",
"in_shopping": "Dans la liste de courses",
"and_up": "&Au-dessus",
"Plan_Show_How_Many_Periods": "Combien de périodes montrer",
"Edit_Meal_Plan_Entry": "Modifier le plan de repas",
"Periods": "Périodes",
"Period": "Période",
"Plan_Period_To_Show": "Montrer les semaines, mois ou années",
"Auto_Planner": "Planning Auto",
"New_Cookbook": "Nouveau livre de recettes",
"Hide_Keyword": "masquer les mots clefs",
"Clear": "Supprimer",
"AddToShopping": "Ajouter à la liste de courses",
"IngredientInShopping": "Cet ingrédient est dans votre liste de courses.",
"NotInShopping": "{food} n'est pas dans votre liste de courses.",
"OnHand": "Disponible actuellement",
"FoodNotOnHand": "L'ingrédient {food} n'est pas disponible.",
"Planner": "Planificateur",
"Planner_Settings": "Paramètres du planificateur",
"AddFoodToShopping": "Ajouter l'ingrédient {food} à votre liste de courses",
"DeleteShoppingConfirm": "Etes-vous sûr que vous souhaitez retirer tous les ingrédients {food} de votre liste de courses ?",
"IgnoredFood": "L'ingrédient {food} est paramétré pour ignorer les courses.",
"Inherit": "Hériter",
"InheritFields": "Hériter les valeurs des champs",
"FoodInherit": "Ingrédient hérité",
"ShowUncategorizedFood": "Montrer ce qui est indéfini",
"GroupBy": "Grouper par",
"SupermarketCategoriesOnly": "Catégories de supermarché uniquement",
"MoveCategory": "Déplacer vers : ",
"IgnoreThis": "Ne jamais ajouter l'ingrédient {food} aux courses",
"DelayFor": "Retard de {hours} heures",
"Warning": "Avertissement",
"InheritWarning": "L'ingrédient {food} est un héritage, les changements pourraient ne pas être conservés.",
"ShowDelayed": "Afficher les éléments retardés",
"Completed": "Achevé",
"shopping_share": "Partager la liste de courses",
"shopping_auto_sync": "Autosynchronisation",
"mealplan_autoadd_shopping": "Ajout automatique d'un plan de repas",
"mealplan_autoexclude_onhand": "Exclure les aliments disponibles",
"mealplan_autoinclude_related": "Ajouter les recettes connexes",
"default_delay": "Heures de retard par défaut",
"mealplan_autoadd_shopping_desc": "Ajouter automatiquement les ingrédients du plan de repas à la liste de courses.",
"mealplan_autoexclude_onhand_desc": "Lorsque vous ajoutez un plan de repas à la liste de courses (manuellement ou automatiquement), excluez les ingrédients que vous avez déjà.",
"default_delay_desc": "Nombre d'heures par défaut pour retarder l'ajoût d'un article à la liste de courses.",
"filter_to_supermarket": "Limiter au supermarché",
"nothing": "Rien à effectuer",
"err_merge_self": "Impossible de fusionner un élément avec lui-même",
"CategoryName": "Intitulé de la catégorie",
"SupermarketName": "Nom du supermarché",
"shopping_recent_days_desc": "Jours des entrées récentes de la liste de courses à afficher.",
"shopping_recent_days": "Jours récents",
"create_shopping_new": "Ajouter à la NOUVELLE liste de courses",
"download_pdf": "Télécharger le PDF",
"download_csv": "Télécharger le CSV",
"csv_delim_help": "Délimiteur à utiliser pour les exports CSV.",
"csv_delim_label": "Délimiteur CSV",
"SuccessClipboard": "Liste de courses copiée dans le presse-papiers",
"copy_to_clipboard": "Copier dans le presse-papiers",
"csv_prefix_help": "Préfixe à ajouter lors de la copie de la liste dans le presse-papiers.",
"csv_prefix_label": "Lister les préfixes",
"copy_markdown_table": "Copier en tant que tableau Markdown",
"DelayUntil": "Retard jusqu'à",
"mark_complete": "Marque comme terminé",
"QuickEntry": "Entrée rapide",
"shopping_add_onhand_desc": "Marquer les aliments comme \"disponibles\" lorsqu'ils sont cochés sur la liste des courses.",
"shopping_add_onhand": "Disponible par défaut",
"related_recipes": "Recettes connexes",
"today_recipes": "Recettes du jour",
"Search Settings": "Paramètres de recherche",
"FoodOnHand": "L'ingrédient {food} est disponible.",
"Undefined": "Indéfini",
"Create_Meal_Plan_Entry": "Création d'un plan de repas",
"RemoveFoodFromShopping": "Retirer l'ingrédient {food} de votre liste de courses"
}

View File

@ -1,8 +1,8 @@
{
"err_fetching_resource": "Si è verificato un errore nel recupero della risorsa!",
"err_fetching_resource": "Si è verificato un errore durante il recupero di una risorsa!",
"err_creating_resource": "Si è verificato un errore durante la creazione di una risorsa!",
"err_updating_resource": "Si è verificato un errore durante l'aggiornamento della risorsa!",
"err_deleting_resource": "Si è verificato un errore durante la cancellazione della risorsa!",
"err_updating_resource": "Si è verificato un errore durante l'aggiornamento di una risorsa!",
"err_deleting_resource": "Si è verificato un errore durante la cancellazione di una risorsa!",
"success_fetching_resource": "Risorsa recuperata con successo!",
"success_creating_resource": "Risorsa creata con successo!",
"success_updating_resource": "Risorsa aggiornata con successo!",
@ -208,5 +208,37 @@
"New_Cookbook": "Nuovo libro di ricette",
"Hide_Keyword": "Nascondi parole chiave",
"Clear": "Pulisci",
"Shopping_List_Empty": "La tua lista della spesa è vuota, puoi aggiungere elementi dal menù contestuale di una voce nel piano alimentare (clicca con il tasto destro sulla scheda o clicca con il tasto sinistro sull'icona del menù)"
"Shopping_List_Empty": "La tua lista della spesa è vuota, puoi aggiungere elementi dal menù contestuale di una voce nel piano alimentare (clicca con il tasto destro sulla scheda o clicca con il tasto sinistro sull'icona del menù)",
"success_moving_resource": "Risorsa spostata con successo!",
"Shopping_Categories": "Categorie di spesa",
"IngredientInShopping": "Questo ingrediente è nella tua lista della spesa.",
"RemoveFoodFromShopping": "Rimuovi {food} dalla tua lista della spesa",
"DelayFor": "Ritarda per {hours} ore",
"OfflineAlert": "Sei offline, le liste della spesa potrebbero non sincronizzarsi.",
"err_moving_resource": "Si è verificato un errore durante lo spostamento di una risorsa!",
"err_merging_resource": "Si è verificato un errore durante l'unione di una risorsa!",
"success_merging_resource": "Risorsa unita con successo!",
"Added_by": "Aggiunto da",
"Added_on": "Aggiunto il",
"AddToShopping": "Aggiungi a lista della spesa",
"NotInShopping": "{food} non è nella tua lista della spesa.",
"Undefined": "Non definito",
"AddFoodToShopping": "Aggiungi {food} alla tua lista della spesa",
"DeleteShoppingConfirm": "Sei sicuro di voler rimuovere tutto {food} dalla lista della spesa?",
"Add_Servings_to_Shopping": "Aggiungi {servings} porzioni alla spesa",
"Inherit": "Eredita",
"InheritFields": "Eredita i valori dei campi",
"ShowUncategorizedFood": "Mostra non definiti",
"GroupBy": "Raggruppa per",
"MoveCategory": "Sposta in: ",
"Warning": "Attenzione",
"NoCategory": "Nessuna categoria selezionata.",
"ShowDelayed": "Mostra elementi ritardati",
"Completed": "Completato",
"shopping_share": "Condividi lista della spesa",
"shopping_auto_sync": "Sincronizzazione automatica",
"err_move_self": "Non è possibile muovere un elemento in sé stesso",
"nothing": "Nulla da fare",
"show_sql": "Mostra SQL",
"Search Settings": "Impostazioni di ricerca"
}

View File

@ -5,7 +5,7 @@
"Log_Recipe_Cooking": "Bereiding loggen",
"External_Recipe_Image": "Externe Afbeelding Recept",
"Add_to_Book": "Voeg toe aan Boek",
"Add_to_Shopping": "Voeg toe aan winkelen",
"Add_to_Shopping": "Voeg toe aan Winkelen",
"Add_to_Plan": "Voeg toe aan Plan",
"Step_start_time": "Starttijd stap",
"Select_Book": "Selecteer boek",
@ -62,15 +62,15 @@
"Files": "Bestanden",
"Size": "Grootte",
"File": "Bestand",
"err_fetching_resource": "Bij het ophalen van een hulpbron is een foutmelding opgetreden!",
"err_creating_resource": "Bij het maken van een hulpbron is een foutmelding opgetreden!",
"err_updating_resource": "Bij het updaten van een hulpbron is een foutmelding opgetreden!",
"err_fetching_resource": "Bij het ophalen van een hulpbron is een fout opgetreden!",
"err_creating_resource": "Bij het maken van een hulpbron is een fout opgetreden!",
"err_updating_resource": "Bij het updaten van een hulpbron is een fout opgetreden!",
"success_fetching_resource": "Hulpbron is succesvol opgehaald!",
"success_creating_resource": "Hulpbron succesvol aangemaakt!",
"success_updating_resource": "Hulpbron succesvol geüpdatet!",
"Success": "Succes",
"Download": "Download",
"err_deleting_resource": "Bij het verwijderen van een hulpbron is een foutmelding opgetreden!",
"err_deleting_resource": "Bij het verwijderen van een hulpbron is een fout opgetreden!",
"success_deleting_resource": "Hulpbron succesvol verwijderd!",
"Cancel": "Annuleer",
"Delete": "Verwijder",
@ -209,5 +209,82 @@
"Auto_Planner": "Autoplanner",
"New_Cookbook": "Nieuw kookboek",
"Hide_Keyword": "Verberg etiketten",
"Clear": "Maak leeg"
"Clear": "Maak leeg",
"Shopping_Categories": "Boodschappen categorieën",
"IngredientInShopping": "Dit ingrediënt staat op je boodschappenlijst.",
"AddFoodToShopping": "Voeg {food} toe aan je boodschappenlijst",
"RemoveFoodFromShopping": "Verwijder {food} van je boodschappenlijst",
"ShowUncategorizedFood": "Toon ongedefinieerd",
"IgnoreThis": "Voeg {food} nooit automatisch toe aan boodschappenlijst",
"OfflineAlert": "Je bent offline, boodschappenlijst synchroniseert mogelijk niet.",
"shopping_share_desc": "Gebruikers zien alle items die je aan je boodschappenlijst toevoegt. Ze moeten jou toevoegen om items op hun lijst te zien.",
"shopping_auto_sync_desc": "Instellen op 0 schakelt automatische synchronisatie uit. Als een boodschappenlijst bekeken wordt, wordt de lijst automatisch elke ingestelde seconden geüpdatet om wijzigingen die iemand anders mogelijk gemaakt heeft te synchroniseren. Dit is nuttig wanneer meerdere mensen gelijktijdig boodschappen aan het doen zijn, maar verbruikt mobiele data.",
"err_moving_resource": "Bij het verplaatsen van een hulpbron is een fout opgetreden!",
"err_merging_resource": "Bij het samenvoegen van een hulpbron is een fout opgetreden!",
"success_moving_resource": "Hulpbron is succesvol verplaatst!",
"success_merging_resource": "Hulpbron is succesvol samengevoegd!",
"err_move_self": "Item kan niet naar zichzelf verplaatst worden",
"Added_by": "Toegevoegd door",
"Added_on": "Toegevoegd op",
"AddToShopping": "Voeg toe aan boodschappenlijst",
"NotInShopping": "{food} staat niet op je boodschappenlijst.",
"OnHand": "Momenteel op voorraad",
"FoodOnHand": "Je hebt {fookd} op voorraad.",
"FoodNotOnHand": "Je hebt {food} niet op voorraad.",
"Undefined": "Ongedefinieerd",
"DeleteShoppingConfirm": "Weet je zeker dat je {food} van de boodschappenlijst wil verwijderen?",
"IgnoredFood": "{food} wordt genegeerd voor winkelen.",
"Add_Servings_to_Shopping": "Voeg {servings} porties toe aan Winkelen",
"Inherit": "Erf",
"InheritFields": "Erf veld waardes",
"FoodInherit": "Eten erfbare velden",
"GroupBy": "Groepeer per",
"SupermarketCategoriesOnly": "Alleen supermarkt categorieën",
"MoveCategory": "Verplaats naar: ",
"CountMore": "...+{count} meer",
"DelayFor": "Stel {hours} uur uit",
"Warning": "Waarschuwing",
"NoCategory": "Geen categorie geselecteerd.",
"InheritWarning": "{food} erft informatie, wijzigingen zijn mogelijk niet blijvend.",
"ShowDelayed": "Toon vertraagde items",
"Completed": "Voltooid",
"shopping_share": "Deel boodschappenlijst",
"shopping_auto_sync": "Synchroniseer automatisch",
"mealplan_autoadd_shopping": "Voeg Maaltijdplan automatisch toe",
"mealplan_autoexclude_onhand": "Sluit eten op voorraad uit",
"mealplan_autoinclude_related": "Voeg gerelateerde recepten toe",
"default_delay": "Standaard vertraging in uren",
"mealplan_autoadd_shopping_desc": "Voeg automatisch ingrediënten uit maaltijdplannen toe aan boodschappenlijst.",
"mealplan_autoexclude_onhand_desc": "Voeg ingrediënten die op voorraad zijn niet toe als een maaltijdplan (handmatig of automatisch) aan de boodschappenlijst toegevoegd wordt.",
"mealplan_autoinclude_related_desc": "Voeg alle gerelateerde recepten van een maaltijdplan toe als een maaltijdplan aan de boodschappenlijst toegevoegd wordt (handmatig of automatisch).",
"default_delay_desc": "Standaard vertraging, in uren, voor een boodschappenlijstitem.",
"filter_to_supermarket": "Filter op supermarkt",
"nothing": "Niks te doen",
"err_merge_self": "Item kan niet met zichzelf samengevoegd worden",
"show_sql": "Toon SQL",
"filter_to_supermarket_desc": "Filter boodschappenlijst om alleen categorieën voor geselecteerde supermarkten te tonen, als standaard.",
"CategoryName": "Naam categorie",
"SupermarketName": "Naam supermarkt",
"CategoryInstruction": "Versleep categorieën om de volgorde waarin ze in de boodschappenlijst getoond worden aan te passen.",
"shopping_recent_days_desc": "Dagen waarop recente boodschappenlijstitems moet worden weergegeven.",
"shopping_recent_days": "Afgelopen dagen",
"create_shopping_new": "Voeg toe aan NIEUWE boodschappenlijst",
"download_pdf": "Download PDF",
"download_csv": "Download CSV",
"csv_delim_help": "Scheidingsteken voor CSV exports.",
"csv_delim_label": "CSV scheidingsteken",
"SuccessClipboard": "Boodschappenlijst is gekopieerd naar klembord",
"copy_to_clipboard": "Kopieer naar klembord",
"csv_prefix_help": "Toe te voegen voorvoegsel als de lijst naar het klembord gekopieerd wordt.",
"csv_prefix_label": "Voorvoegsel van lijst",
"copy_markdown_table": "Kopieer als markdown tabel",
"in_shopping": "Op boodschappenlijst",
"DelayUntil": "Vertraag tot",
"mark_complete": "Voltooid",
"QuickEntry": "Snelle invoer",
"shopping_add_onhand_desc": "Vink eten 'op voorraad' af van boodschappenlijst.",
"shopping_add_onhand": "Automatisch op voorraad",
"related_recipes": "Gerelateerde recepten",
"today_recipes": "Recepten van vandaag",
"Search Settings": "Zoekinstellingen"
}

View File

@ -1,5 +1,5 @@
{
"warning_feature_beta": "Ta funkcja jest obecnie w wersji BETA (testowej). Podczas korzystania z tej funkcji należy spodziewać się błędów i ewentualnych zmian w przyszłości (prawdopodobna utrata danych powiązanych z tą funkcją).",
"warning_feature_beta": "Ta funkcja jest obecnie w wersji BETA (testowej). Podczas korzystania z tej funkcji mogą wystąpić błędy, a w przyszłości zmiany funkcjonalności (możliwa utrata danych powiązanych z tą funkcją).",
"err_fetching_resource": "Wystąpił błąd podczas pobierania zasobu!",
"err_creating_resource": "Wystąpił błąd podczas tworzenia zasobu!",
"err_updating_resource": "Wystąpił błąd podczas aktualizowania zasobu!",
@ -207,5 +207,83 @@
"Auto_Planner": "Plan automatyczny",
"New_Cookbook": "Nowa książka kucharska",
"Hide_Keyword": "Ukryj słowa kluczowe",
"Clear": "Wyczyść"
"Clear": "Wyczyść",
"err_moving_resource": "Wystąpił błąd podczas przenoszenia zasobu!",
"err_merging_resource": "Wystąpił błąd podczas scalania zasobu!",
"success_moving_resource": "Pomyślnie przeniesiono zasób!",
"success_merging_resource": "Pomyślnie scalono zasób!",
"Added_by": "Dodane przez",
"Added_on": "Dodano dnia",
"IngredientInShopping": "Ten składnik znajduje się na Twojej liście zakupów.",
"NotInShopping": "{food} nie ma na Twojej liście zakupów.",
"OnHand": "Obecnie posiadane",
"FoodNotOnHand": "Nie posiadasz {food}.",
"Undefined": "Nieokreślony",
"AddFoodToShopping": "Dodaj {food} do swojej listy zakupów",
"RemoveFoodFromShopping": "Usuń {food} z listy zakupów",
"Shopping_Categories": "Kategorie zakupów",
"AddToShopping": "Dodaj do listy zakupów",
"FoodOnHand": "Posiadasz {food}.",
"DeleteShoppingConfirm": "Czy na pewno chcesz usunąć wszystkie {food} z listy zakupów?",
"CountMore": "...+{count} więcej",
"InheritWarning": "{food} jest ustawiony aby dziedziczyć, zmiany mogą się nie zachować.",
"mealplan_autoadd_shopping": "Automatyczne dodawanie planów posiłków",
"mealplan_autoexclude_onhand_desc": "Dodając plan posiłków do listy zakupów (ręcznie lub automatycznie), wyklucz składniki, które aktualnie posiadasz.",
"shopping_share_desc": "Użytkownicy zobaczą wszystkie przedmioty, które dodasz do swojej listy zakupów. Muszą Cię dodać, aby zobaczyć przedmioty na ich liście.",
"shopping_auto_sync_desc": "Ustawienie 0 spowoduje wyłączenie auto synchronizacji. Podczas przeglądania listy zakupów lista jest aktualizowana co kilka sekund, aby zsynchronizować zmiany, które mógł wprowadzić ktoś inny. Przydatne podczas robienia zakupów w kilka osób, ale będzie korzystać z transmisji danych.",
"filter_to_supermarket_desc": "Domyślnie filtruj listę zakupów, aby zawierała tylko kategorie dla wybranego supermarketu.",
"csv_delim_help": "Separator używany przy eksporcie CSV.",
"csv_prefix_help": "Prefiks do dodania podczas kopiowania listy do schowka.",
"IgnoredFood": "{food} jest ustawiony jako ignorowany w zakupach.",
"Add_Servings_to_Shopping": "Dodaj {servings} porcje do zakupów",
"Inherit": "Dziedziczenie",
"InheritFields": "Dziedziczenie wartości pól",
"FoodInherit": "Pola dziedziczone w żywności",
"ShowUncategorizedFood": "Pokaż niezdefiniowane",
"GroupBy": "Grupuj według",
"SupermarketCategoriesOnly": "Tylko kategorie supermarketów",
"MoveCategory": "Przenieś do: ",
"IgnoreThis": "Nigdy nie dodawaj automatycznie {food} do zakupów",
"DelayFor": "Opóźnij o {hours} godzin",
"Warning": "Ostrzeżenie",
"NoCategory": "Nie wybrano kategorii.",
"ShowDelayed": "Pokaż opóźnione elementy",
"Completed": "Zakończone",
"OfflineAlert": "Jesteś offline, lista zakupów może nie być zsynchronizowana.",
"shopping_share": "Udostępnij listę zakupów",
"shopping_auto_sync": "Automatyczna synchronizacja",
"mealplan_autoexclude_onhand": "Wyklucz posiadane jedzenie",
"mealplan_autoinclude_related": "Dodaj powiązane przepisy",
"default_delay": "Domyślna ilość godzin opóźnienia",
"mealplan_autoadd_shopping_desc": "Automatycznie dodawaj składniki z planu posiłków do listy zakupów.",
"mealplan_autoinclude_related_desc": "Dodając plan posiłków do listy zakupów (ręcznie lub automatycznie), uwzględnij wszystkie powiązane przepisy.",
"default_delay_desc": "Domyślna liczba godzin opóźnienia dla wpisu na listę zakupów.",
"err_move_self": "Nie można przenieść elementu do niego samego",
"nothing": "Nie ma nic do zrobienia",
"err_merge_self": "Nie można scalić elementu z nim samym",
"show_sql": "Pokaż SQL",
"CategoryName": "Nazwa kategorii",
"SupermarketName": "Nazwa supermarketu",
"filter_to_supermarket": "Filtruj po supermarkecie",
"CategoryInstruction": "Przeciągnij kategorie, aby zmienić kolejność w jakiej kategorie pojawiają się na liście zakupów.",
"shopping_recent_days_desc": "Dni ostatnich wpisów do wyświetlenia na liście zakupów.",
"shopping_recent_days": "Ostatnie dni",
"create_shopping_new": "Dodaj do NOWEJ listy zakupów",
"download_pdf": "Pobierz PDF",
"download_csv": "Pobierz CSV",
"csv_delim_label": "Separator CSV",
"SuccessClipboard": "Lista zakupów skopiowana do schowka",
"copy_to_clipboard": "Skopiuj do schowka",
"csv_prefix_label": "Prefiks listy",
"copy_markdown_table": "Kopiuj jako Tablicę Markdown",
"in_shopping": "Na liście zakupów",
"DelayUntil": "Opóźnij do",
"shopping_add_onhand_desc": "Zaznacz jedzenie „Posiadam”, gdy jest odhaczone na liście zakupów.",
"shopping_add_onhand": "Auto \"Posiadam\"",
"mark_complete": "Oznacz jako ukończone",
"QuickEntry": "Szybki wpis",
"related_recipes": "Powiązane przepisy",
"today_recipes": "Dzisiejsze przepisy",
"Search Settings": "Ustawienia wyszukiwania",
"Pin": "Pin"
}

View File

@ -1,210 +1,288 @@
{
"warning_feature_beta": "Ta funkcija je trenutno v stanju BETA (testiranje). Pri uporabi te funkcije pričakujte napake in morebitne prelomne spremembe v prihodnosti (morda izgubite podatke, povezane s to funkcijo).",
"err_fetching_resource": "",
"err_creating_resource": "",
"err_updating_resource": "",
"err_deleting_resource": "",
"success_fetching_resource": "",
"success_creating_resource": "",
"success_updating_resource": "",
"success_deleting_resource": "",
"file_upload_disabled": "",
"step_time_minutes": "",
"confirm_delete": "",
"import_running": "",
"all_fields_optional": "",
"convert_internal": "",
"show_only_internal": "",
"show_split_screen": "",
"Log_Recipe_Cooking": "",
"External_Recipe_Image": "",
"Add_to_Shopping": "",
"Add_to_Plan": "",
"Step_start_time": "",
"Sort_by_new": "",
"Table_of_Contents": "",
"Recipes_per_page": "",
"Show_as_header": "",
"Hide_as_header": "",
"Add_nutrition_recipe": "",
"Remove_nutrition_recipe": "",
"Copy_template_reference": "",
"Save_and_View": "",
"Manage_Books": "",
"Meal_Plan": "",
"Select_Book": "",
"Select_File": "",
"Recipe_Image": "",
"Import_finished": "",
"View_Recipes": "",
"Log_Cooking": "",
"err_fetching_resource": "Napaka pri pridobivanju vira!",
"err_creating_resource": "Napaka pri ustvarjanju vira!",
"err_updating_resource": "Napaka pri posodabljanju vira!",
"err_deleting_resource": "Napaka pri brisanju vira!",
"success_fetching_resource": "Pridobivanje vira je bilo uspešno!",
"success_creating_resource": "Ustvarjanje vira je bilo uspešno!",
"success_updating_resource": "Posodabljanje vira je bilo uspešno!",
"success_deleting_resource": "Brisanje vira je bilo uspešno!",
"file_upload_disabled": "Nalaganje datoteke ni omogočeno za tvoj prostor.",
"step_time_minutes": "Časovni korak v minutah",
"confirm_delete": "Ali si prepričan da želiš izbrisati {object}?",
"import_running": "Uvoz poteka, prosim počakaj!",
"all_fields_optional": "Vsa polja so opcijska in jih lahko pustiš prazne.",
"convert_internal": "Pretvori v interni recept",
"show_only_internal": "Prikaži samo interne recepte",
"show_split_screen": "Deljen pogled",
"Log_Recipe_Cooking": "Logiraj recept za kuhanje",
"External_Recipe_Image": "Zunanja slika recepta",
"Add_to_Shopping": "Dodaj v nakupovalni listek",
"Add_to_Plan": "Dodaj v načrt",
"Step_start_time": "Začetni čas koraka",
"Sort_by_new": "Razvrsti po novih",
"Table_of_Contents": "Kazalo vsebine",
"Recipes_per_page": "Receptov na stran",
"Show_as_header": "Prikaži kot glavo",
"Hide_as_header": "Skrij kot glavo",
"Add_nutrition_recipe": "Receptu dodaj hranilno vrednost",
"Remove_nutrition_recipe": "Receptu izbriši hranilno vrednost",
"Copy_template_reference": "Kopiraj referenco vzorca",
"Save_and_View": "Shrani in poglej",
"Manage_Books": "Upravljaj knjige",
"Meal_Plan": "Načrt obroka",
"Select_Book": "Izberi knjigo",
"Select_File": "Izberi datoteko",
"Recipe_Image": "Slika recepta",
"Import_finished": "Uvoz je končan",
"View_Recipes": "Preglej recepte",
"Log_Cooking": "Zgodovina kuhanja",
"New_Recipe": "Nov Recept",
"Url_Import": "",
"Reset_Search": "",
"Recently_Viewed": "",
"Load_More": "",
"New_Keyword": "",
"Delete_Keyword": "",
"Edit_Keyword": "",
"Url_Import": "URL uvoz",
"Reset_Search": "Ponastavi iskalnik",
"Recently_Viewed": "Nazadnje videno",
"Load_More": "Naloži več",
"New_Keyword": "Nova ključna beseda",
"Delete_Keyword": "Izbriši ključno besedo",
"Edit_Keyword": "Uredi ključno besedo",
"Edit_Recipe": "Uredi Recept",
"Move_Keyword": "",
"Merge_Keyword": "",
"Hide_Keywords": "",
"Hide_Recipes": "",
"Move_Up": "",
"Move_Down": "",
"Step_Name": "",
"Step_Type": "",
"Make_header": "",
"Make_Ingredient": "",
"Enable_Amount": "",
"Disable_Amount": "",
"Add_Step": "",
"Keywords": "",
"Move_Keyword": "Premakni ključno besedo",
"Merge_Keyword": "Združi ključno besedo",
"Hide_Keywords": "Skrij ključno besedo",
"Hide_Recipes": "Skrij recept",
"Move_Up": "Premakni navzgor",
"Move_Down": "Premakni navzdol",
"Step_Name": "Ime koraka",
"Step_Type": "Tip koraka",
"Make_header": "Ustvari_glavo",
"Make_Ingredient": "Ustvari_sestavino",
"Enable_Amount": "Omogoči količino",
"Disable_Amount": "Onemogoči količino",
"Add_Step": "Dodaj korak",
"Keywords": "Ključne besede",
"Books": "Knjige",
"Proteins": "",
"Fats": "",
"Carbohydrates": "",
"Calories": "",
"Energy": "",
"Nutrition": "",
"Proteins": "Beljakovine",
"Fats": "Maščobe",
"Carbohydrates": "Ogljikovi hidrati",
"Calories": "Kalorije",
"Energy": "Energija",
"Nutrition": "Prehrana",
"Date": "Datum",
"Share": "Deli",
"Automation": "",
"Parameter": "",
"Export": "",
"Copy": "",
"Rating": "",
"Close": "",
"Cancel": "",
"Link": "",
"Add": "",
"New": "",
"Note": "",
"Success": "",
"Failure": "",
"Ingredients": "",
"Supermarket": "",
"Categories": "",
"Category": "",
"Selected": "",
"min": "",
"Servings": "",
"Waiting": "",
"Preparation": "",
"External": "",
"Size": "",
"Files": "",
"File": "",
"Edit": "",
"Image": "",
"Automation": "Avtomatizacija",
"Parameter": "Parameter",
"Export": "Izvoz",
"Copy": "Kopiraj",
"Rating": "Ocena",
"Close": "Zapri",
"Cancel": "Prekini",
"Link": "Hiperpovezava",
"Add": "Dodaj",
"New": "Nov",
"Note": "Opomba",
"Success": "Uspešno",
"Failure": "Napaka",
"Ingredients": "Sestavine",
"Supermarket": "Supermarket",
"Categories": "Kategorije",
"Category": "Kategorija",
"Selected": "Izbrano",
"min": "min",
"Servings": "Porcije",
"Waiting": "Čakanje",
"Preparation": "Priprava",
"External": "Zunanje",
"Size": "Velikost",
"Files": "Datoteke",
"File": "Datoteka",
"Edit": "Uredi",
"Image": "Slika",
"Delete": "Izbriši",
"Open": "Odpri",
"Ok": "Odpri",
"Save": "Shrani",
"Step": "",
"Step": "Korak",
"Search": "Iskanje",
"Import": "Uvozi",
"Print": "Natisni",
"Settings": "",
"or": "",
"and": "",
"Information": "",
"Settings": "Nastavitve",
"or": "ali",
"and": "in",
"Information": "Informacija",
"Download": "Prenesi",
"Create": "",
"Create": "Ustvari",
"Advanced Search Settings": "",
"View": "",
"View": "Pogled",
"Recipes": "Recepti",
"Move": "",
"Merge": "",
"Parent": "",
"delete_confirmation": "",
"move_confirmation": "",
"merge_confirmation": "",
"create_rule": "",
"move_selection": "",
"merge_selection": "",
"Move": "Premakni",
"Merge": "Združi",
"Parent": "Starš",
"delete_confirmation": "Ste prepričani da želite odstraniti {source}?",
"move_confirmation": "Premakni <i>{child}</i> k staršu <i>{parent}</i>",
"merge_confirmation": "Zamenjaj <i>{source}</i> z/s <i>{target}</i>",
"create_rule": "in ustvari avtomatizacijo",
"move_selection": "Izberi starša {type} za premik v {source}.",
"merge_selection": "Zamenjaj vse dogodge {source} z izbranim {type}.",
"Root": "",
"Ignore_Shopping": "",
"Shopping_Category": "",
"Edit_Food": "",
"Move_Food": "",
"New_Food": "",
"Hide_Food": "",
"Food_Alias": "",
"Unit_Alias": "",
"Keyword_Alias": "",
"Delete_Food": "",
"No_ID": "",
"Meal_Plan_Days": "",
"merge_title": "",
"move_title": "",
"Ignore_Shopping": "Prezri nakup",
"Shopping_Category": "Kategorija nakupa",
"Edit_Food": "Uredi hrano",
"Move_Food": "Premakni hrano",
"New_Food": "Nova hrana",
"Hide_Food": "Skrij hrano",
"Food_Alias": "Vzdevek hrane",
"Unit_Alias": "Vzdevek enote",
"Keyword_Alias": "Vzdevek ključne besede",
"Delete_Food": "Izbriši hrano",
"No_ID": "ID ni najden, ne morem izbrisati.",
"Meal_Plan_Days": "Načrt za prihodnje obroke",
"merge_title": "Združi {type}",
"move_title": "Premakni {type}",
"Food": "Hrana",
"Recipe_Book": "",
"del_confirmation_tree": "",
"delete_title": "",
"create_title": "",
"edit_title": "",
"Name": "",
"Type": "",
"Description": "",
"Recipe": "",
"Recipe_Book": "Knjiga receptov",
"del_confirmation_tree": "Si prepričan/a, da želiš izbrisati {source} in vse podkategorije?",
"delete_title": "Izbriši {type}",
"create_title": "Novo {type}",
"edit_title": "Uredi {type}",
"Name": "Ime",
"Type": "Tip",
"Description": "Opis",
"Recipe": "Recept",
"tree_root": "",
"Icon": "",
"Unit": "",
"No_Results": "",
"New_Unit": "",
"Create_New_Shopping Category": "",
"Icon": "Ikona",
"Unit": "Enota",
"No_Results": "Ni rezultatov",
"New_Unit": "Nova enota",
"Create_New_Shopping Category": "Ustvari novo kategorijo nakupovalnega listka",
"Create_New_Food": "Dodaj Novo Hrano",
"Create_New_Keyword": "",
"Create_New_Unit": "",
"Create_New_Meal_Type": "",
"and_up": "",
"Instructions": "",
"Unrated": "",
"Automate": "",
"Empty": "",
"Key_Ctrl": "",
"Key_Shift": "",
"Time": "",
"Text": "",
"Create_New_Keyword": "Dodaj novo ključno besedo",
"Create_New_Unit": "Dodaj novo enoto",
"Create_New_Meal_Type": "Dodaj nov tip obroka",
"and_up": "& gor",
"Instructions": "Navodila",
"Unrated": "Neocenjeno",
"Automate": "Avtomatiziraj",
"Empty": "Prazno",
"Key_Ctrl": "Ctrl",
"Key_Shift": "Shift",
"Time": "Čas",
"Text": "Tekst",
"Shopping_list": "Nakupovalni Seznam",
"Create_Meal_Plan_Entry": "",
"Edit_Meal_Plan_Entry": "",
"Title": "",
"Create_Meal_Plan_Entry": "Ustvari vnos za načrtovan obrok",
"Edit_Meal_Plan_Entry": "Spremeni vnos za načrtovan obrok",
"Title": "Naslov",
"Week": "Teden",
"Month": "Mesec",
"Year": "Leto",
"Planner": "",
"Planner_Settings": "",
"Period": "",
"Plan_Period_To_Show": "",
"Periods": "",
"Plan_Show_How_Many_Periods": "",
"Starting_Day": "",
"Meal_Types": "",
"Meal_Type": "",
"Clone": "",
"Drag_Here_To_Delete": "",
"Meal_Type_Required": "",
"Title_or_Recipe_Required": "",
"Planner": "Planer",
"Planner_Settings": "Nastavitve planerja",
"Period": "Obdobje",
"Plan_Period_To_Show": "Prikaži, tedne, mesece ali leta",
"Periods": "Obdobja",
"Plan_Show_How_Many_Periods": "Koliko obdobij prikažem",
"Starting_Day": "Začetni dan v tednu",
"Meal_Types": "Tipi obroka",
"Meal_Type": "Tip obroka",
"Clone": "Kloniraj",
"Drag_Here_To_Delete": "Povleci sem za izbris",
"Meal_Type_Required": "Tip obroka je obvezen",
"Title_or_Recipe_Required": "Zahtevan je naslov ali izbran recept",
"Color": "Barva",
"New_Meal_Type": "",
"Week_Numbers": "",
"Show_Week_Numbers": "",
"Export_As_ICal": "",
"Export_To_ICal": "",
"Cannot_Add_Notes_To_Shopping": "",
"Added_To_Shopping_List": "",
"Shopping_List_Empty": "",
"Next_Period": "",
"Previous_Period": "",
"Current_Period": "",
"New_Meal_Type": "Nov tip obroka",
"Week_Numbers": "Števila tednov",
"Show_Week_Numbers": "Prikaži število tednov?",
"Export_As_ICal": "Izvozi trenutno obdobje v iCal format",
"Export_To_ICal": "Izvoz.ics",
"Cannot_Add_Notes_To_Shopping": "Opombe ne moreš dodati v nakupovalni listek",
"Added_To_Shopping_List": "Dodano v nakupovalni listek",
"Shopping_List_Empty": "Tvoj nakupovalni listek je trenutno prazen. Stvari lahko dodaš preko menija za načrt obroka (desni klik na kartico ali levi klik na ikono za meni)",
"Next_Period": "Naslednje obdobje",
"Previous_Period": "Prejšnje obdobje",
"Current_Period": "Trenutno obdobje",
"Next_Day": "Naslednji Dan",
"Previous_Day": "Prejšnji Dan",
"Coming_Soon": "",
"Auto_Planner": "",
"New_Cookbook": "",
"Hide_Keyword": "",
"Clear": ""
"Coming_Soon": "Kmalu",
"Auto_Planner": "Avto-planer",
"New_Cookbook": "Nova kuharska knjiga",
"Hide_Keyword": "Skrij ključne besede",
"Clear": "Počisti",
"Pin": "Pripni",
"err_moving_resource": "Napaka pri premikanju vira!",
"err_merging_resource": "Napaka pri združevanju vira!",
"Shopping_Categories": "Kategorije nakupa",
"IngredientInShopping": "Ta sestavina je v tvojem nakupovalnem listku.",
"RemoveFoodFromShopping": "Odstrani {food} iz nakupovalnega listka",
"SupermarketCategoriesOnly": "Prikaži samo trgovinske kategorije",
"DelayFor": "Zamakni za {hours} ur",
"OfflineAlert": "Si v offline načinu, nakupovalni listek se mogoče ne bo sinhroniziral.",
"shopping_share_desc": "Uporabniki bodo videli vse elemente, ki si jih dodal v nakupovalni listek. Morajo te dodati, da vidiš njihove elemente na listku.",
"shopping_auto_sync_desc": "Nastavitev na 0 bo onemogoča avtomatsko sinhronizacijo. Pri ogledu nakupovalnega seznama se seznam posodablja vsakih nekaj sekund za sinhronizacijo sprememb, ki jih je morda naredil nekdo drug. Uporabno pri nakupovanju z več ljudmi, vendar bo uporabljalo mobilne podatke.",
"filter_to_supermarket_desc": "Privzeto, razvrsti nakupovalni listek, da vključi samo označene trgovine.",
"SuccessClipboard": "Nakupovalni listek je kopiran v odložišče",
"left_handed": "Način za levičarje",
"left_handed_help": "Optimizira grafični vmesnik za levičarje.",
"success_moving_resource": "Premikanje vira je bilo uspešno!",
"success_merging_resource": "Združevanje vira je bilo uspešno!",
"Added_by": "Dodano s strani",
"AddToShopping": "Dodaj nakupovlanemu listku",
"NotInShopping": "{food} ni v tvojem nakupovalnem listku.",
"OnHand": "Trenutno imam v roki",
"FoodOnHand": "Imaš {food} v roki.",
"FoodNotOnHand": "Nimaš {food} v roki.",
"Undefined": "Nedefiniran",
"AddFoodToShopping": "Dodaj {food} v nakupovalni listek",
"DeleteShoppingConfirm": "Si prepričan/a, da želiš odstraniti VSO {food} iz nakupovalnega listka?",
"Inherit": "Podeduj",
"InheritFields": "Podeduj vrednosti polja",
"FoodInherit": "Podedovana polja hrane",
"ShowUncategorizedFood": "Prikaži nedefinirano",
"GroupBy": "Združi po",
"MoveCategory": "Premakni v: ",
"CountMore": "...+{count} več",
"IgnoreThis": "Nikoli avtomatsko ne dodaj {food} v nakup",
"Warning": "Opozorilo",
"NoCategory": "Nobena kategorija ni izbrana.",
"InheritWarning": "{food} je nastavljena na dedovanje, spremembe morda ne bodo trajale.",
"ShowDelayed": "Prikaži zamaknjene elemente",
"Completed": "Končano",
"shopping_share": "Deli nakupovalni listek",
"shopping_auto_sync": "Avtomatska sinhronizacija",
"mealplan_autoadd_shopping": "Avtomatsko dodaj obrok v načrt",
"mealplan_autoexclude_onhand": "Izključi hrano v roki",
"mealplan_autoinclude_related": "Dodaj povezane recepte",
"default_delay": "Privzete ure za zamik",
"mealplan_autoadd_shopping_desc": "Avtomatsko dodaj sestavine načrtovanega obroka v nakupovalni listek.",
"mealplan_autoinclude_related_desc": "Pri dodajanju načrta obrokov na nakupovalni seznam (ročno ali samodejno) vključi sestavine, ki so povezane z receptom.",
"mealplan_autoexclude_onhand_desc": "Pri dodajanju načrta obrokov na nakupovalni seznam (ročno ali samodejno) izključite sestavine, ki so trenutno v roki.",
"err_move_self": "Ne morem premakniti elementa v samega sebe",
"nothing": "Ni kaj za narediti",
"err_merge_self": "Ne morem združiti elementa v samega sebe",
"show_sql": "Prikaži SQL",
"CategoryName": "Ime kategorije",
"SupermarketName": "Ime trgovine",
"CategoryInstruction": "Povleci kategorije za spremembo vrstnega reda v nakupovalnem listku.",
"shopping_recent_days_desc": "Dnevi nedavnih vnosov na seznamu za nakupovanje, ki jih želite prikazati.",
"shopping_recent_days": "Nedavni dnevi",
"create_shopping_new": "Dodaj v NOV nakupovalni listek",
"download_pdf": "Prenesi PDF",
"download_csv": "Prenesi CSV",
"csv_delim_help": "Ločilo za CSV izvoz.",
"csv_delim_label": "CSV ločilo",
"copy_to_clipboard": "Kopiraj v odložiče",
"csv_prefix_help": "Dodana prepona, ko kopiramo nakupovalni listek v odložišče.",
"csv_prefix_label": "Prepona seznama",
"copy_markdown_table": "Kopiraj kot Markdown tabela",
"in_shopping": "V nakupovalnem listku",
"DelayUntil": "Zamakni do",
"shopping_add_onhand": "Avtomatsko v roki",
"related_recipes": "Povezani recepti",
"today_recipes": "Današnji recepti",
"mark_complete": "Označi končano",
"QuickEntry": "Hitri vnos",
"Search Settings": "Išči nastavitev",
"sql_debug": "SQL razhroščevanje",
"remember_search": "Zapomni si iskanje",
"remember_hours": "Ure, ki si jih zapomni",
"tree_select": "Uporabi drevesno označbo"
}

View File

@ -1,47 +1,47 @@
{
"err_fetching_resource": "",
"err_creating_resource": "",
"err_updating_resource": "",
"err_deleting_resource": "",
"success_fetching_resource": "",
"success_creating_resource": "",
"success_updating_resource": "",
"success_deleting_resource": "",
"import_running": "",
"all_fields_optional": "",
"convert_internal": "",
"show_only_internal": "",
"Log_Recipe_Cooking": "",
"err_fetching_resource": "获取资源时出错!",
"err_creating_resource": "创建资源时出错!",
"err_updating_resource": "更新资源时出错!",
"err_deleting_resource": "删除资源时出错!",
"success_fetching_resource": "已成功获取资源!",
"success_creating_resource": "已成功创建资源!",
"success_updating_resource": "已成功更新资源!",
"success_deleting_resource": "已成功删除资源!",
"import_running": "正在导入,请稍候!",
"all_fields_optional": "所有字段都是可选的,可以留空。",
"convert_internal": "转换为内部菜谱",
"show_only_internal": "仅显示内部菜谱",
"Log_Recipe_Cooking": "菜谱烹饪记录",
"External_Recipe_Image": "外部菜谱图像",
"Add_to_Shopping": "添加到购物",
"Add_to_Plan": "添加到计划",
"Step_start_time": "",
"Sort_by_new": "",
"Recipes_per_page": "",
"Step_start_time": "步骤开始时间",
"Sort_by_new": "按新旧排序",
"Recipes_per_page": "每页菜谱数量",
"Manage_Books": "管理书籍",
"Meal_Plan": "",
"Select_Book": "",
"Meal_Plan": "用餐计划",
"Select_Book": "选择书籍",
"Recipe_Image": "菜谱图像",
"Import_finished": "导入完成",
"View_Recipes": "",
"Log_Cooking": "",
"View_Recipes": "查看菜谱",
"Log_Cooking": "烹饪记录",
"New_Recipe": "新菜谱",
"Url_Import": "导入网址",
"Reset_Search": "重置搜索",
"Recently_Viewed": "最近浏览",
"Load_More": "加载更多",
"Keywords": "关键",
"Keywords": "关键",
"Books": "书籍",
"Proteins": "蛋白质",
"Fats": "脂肪",
"Carbohydrates": "碳水化合物",
"Calories": "卡路里",
"Energy": "",
"Energy": "能量",
"Nutrition": "营养",
"Date": "日期",
"Share": "分享",
"Export": "导出",
"Copy": "拷贝",
"Copy": "复制",
"Rating": "评分",
"Close": "关闭",
"Link": "链接",
@ -50,11 +50,11 @@
"Success": "成功",
"Failure": "失败",
"Ingredients": "材料",
"Supermarket": "超级市场",
"Supermarket": "超",
"Categories": "分类",
"Category": "分类",
"Selected": "选定",
"min": "",
"min": "分钟",
"Servings": "份量",
"Waiting": "等待",
"Preparation": "准备",
@ -67,15 +67,213 @@
"Delete": "删除",
"Open": "打开",
"Ok": "打开",
"Save": "存",
"Save": "存",
"Step": "步骤",
"Search": "搜索",
"Import": "导入",
"Print": "打印",
"Settings": "设置",
"or": "或",
"and": "",
"Information": "更多资讯",
"and": "",
"Information": "更多信息",
"Download": "下载",
"Create": "创"
"Create": "创",
"Table_of_Contents": "目录",
"Delete_Keyword": "删除关键词",
"Edit_Keyword": "编辑关键词",
"New_Keyword": "新关键词",
"Select_File": "选择文件",
"Merge_Keyword": "合并关键词",
"Hide_Keywords": "隐藏关键词",
"Image": "图片",
"Recipes": "菜谱",
"Move": "移动",
"Merge": "合并",
"confirm_delete": "您确定要删除 {object} 吗?",
"Save_and_View": "保存并查看",
"Edit_Recipe": "编辑菜谱",
"Move_Up": "上移",
"show_split_screen": "拆分视图",
"Move_Keyword": "移动关键词",
"Hide_Recipes": "隐藏菜谱",
"Move_Down": "下移",
"Step_Name": "步骤名",
"Step_Type": "步骤类型",
"Enable_Amount": "启用金额",
"Disable_Amount": "禁用金额",
"Add_Step": "添加步骤",
"delete_confirmation": "你确定要删除 {source} 吗?",
"Search Settings": "搜索设置",
"Key_Shift": "Shift",
"Key_Ctrl": "Ctrl",
"Parameter": "范围",
"edit_title": "编辑 {type}",
"Type": "类型",
"Name": "名字",
"Description": "描述",
"create_title": "新建 {type}",
"move_confirmation": "移动 <i>{child}</i> 到 <i>{parent}</i>",
"create_rule": "并创建自动化",
"move_title": "移动 {type}",
"merge_title": "合并 {type}",
"Create_New_Unit": "添加新的单位",
"Create_New_Food": "添加新的食物",
"warning_feature_beta": "此功能目前处于测试状态。在使用此功能时,请做好将来会出现错误和破坏性更改(可能会丢失与功能相关的数据)的准备。",
"Add_nutrition_recipe": "将营养信息添加到菜谱中",
"Remove_nutrition_recipe": "从菜谱中删除营养信息",
"Keyword_Alias": "关键词别名",
"Create_New_Meal_Type": "添加新的用餐类型",
"Time": "时间",
"Text": "文本",
"CategoryName": "分类名",
"Automation": "自动化",
"Parent": "父级",
"Unrated": "未评分",
"Shopping_Categories": "购物类别",
"del_confirmation_tree": "你确定要删除 {source} 及其所有子项吗?",
"Create_New_Keyword": "添加新的关键词",
"Added_by": "添加者",
"Shopping_list": "采购单",
"Recipe": "菜谱",
"file_upload_disabled": "你的空间未启用文件上传。",
"delete_title": "删除 {type}",
"tree_root": "树根",
"Unit": "单位",
"No_Results": "没有结果",
"New_Unit": "新建单位",
"Ignore_Shopping": "忽略购物",
"Shopping_Category": "购物类别",
"Edit_Food": "编辑食物",
"Move_Food": "移动食物",
"step_time_minutes": "步骤耗时(分钟)",
"Copy_template_reference": "复制模板参考",
"err_moving_resource": "移动资源时出错!",
"err_merging_resource": "合并资源时出错!",
"success_moving_resource": "已成功移动资源!",
"success_merging_resource": "已成功合并资源!",
"View": "查看",
"New_Food": "新建食物",
"Hide_Food": "隐藏食物",
"Food_Alias": "食物别名",
"Unit_Alias": "单位别名",
"Delete_Food": "删除食物",
"No_ID": "未找到标识,不能删除。",
"Meal_Plan_Days": "未来的用餐计划",
"Food": "食物",
"Recipe_Book": "菜谱书",
"Icon": "图标",
"Create_New_Shopping Category": "创建新的购物类别",
"Automate": "自动化",
"Empty": "空的",
"SupermarketName": "超市名",
"NotInShopping": "购物清单中没有 {food}。",
"Drag_Here_To_Delete": "拖动此处可删除",
"DeleteShoppingConfirm": "确定要移除购物清单中所有 {food} 吗?",
"ShowDelayed": "显示延迟的项目",
"mealplan_autoexclude_onhand": "排除入手的食物",
"shopping_share_desc": "用户将看到您添加到购物清单中的所有商品。他们必须添加你才能看到他们清单上的内容。",
"shopping_auto_sync_desc": "设置为0将禁用自动同步。当查看购物列表时该列表每隔一秒更新一次以同步其他人可能做出的更改。在多人购物时很有用但会使用移动数据。",
"err_merge_self": "无法将项目与自身合并",
"CategoryInstruction": "拖动类别可更改出现在购物清单中的订单类别。",
"csv_prefix_help": "将清单复制到剪贴板时要添加的前缀。",
"remember_search": "记住搜索",
"Root": "根",
"Instructions": "说明",
"Period": "周期",
"Plan_Period_To_Show": "显示星期、月或年",
"Periods": "周期",
"Plan_Show_How_Many_Periods": "要显示多少个周期",
"Starting_Day": "一周中的第一天",
"Meal_Types": "用餐类型",
"Make_header": "显示注意事项",
"Color": "颜色",
"New_Meal_Type": "新用餐类型",
"Pin": "固定",
"Planner_Settings": "计划者设置",
"Meal_Type": "用餐类型",
"Clone": "复制",
"Title_or_Recipe_Required": "需要选择标题或菜谱",
"Export_As_ICal": "将当前周期导出为 iCal 格式",
"Week_Numbers": "周数",
"Show_Week_Numbers": "显示周数?",
"Coming_Soon": "即将到来",
"New_Cookbook": "新烹饪书",
"Hide_Keyword": "隐藏关键词",
"Export_To_ICal": "导出 .ics",
"Added_To_Shopping_List": "添加到购物清单",
"Cannot_Add_Notes_To_Shopping": "无法将笔记添加到购物清单",
"Shopping_List_Empty": "您的购物列表当前为空,您可以通过用餐计划条目的上下文菜单添加项目(右键单击卡片或左键单击菜单图标)",
"Next_Period": "下期",
"Current_Period": "本期",
"Next_Day": "第二天",
"Previous_Period": "上期",
"Previous_Day": "前一天",
"remember_hours": "需要记住的时间",
"tree_select": "使用树形选择",
"Make_Ingredient": "显示材料",
"Note": "笔记",
"Added_on": "添加到",
"AddToShopping": "添加到购物清单",
"IngredientInShopping": "此材料已在购物清单。",
"OnHand": "目前",
"FoodOnHand": "你手上有 {food}。",
"FoodNotOnHand": "你还没有 {food}。",
"Undefined": "未定义的",
"Create_Meal_Plan_Entry": "创建用餐计划条目",
"Edit_Meal_Plan_Entry": "编辑用餐计划条目",
"Title": "标题",
"Week": "星期",
"Month": "月份",
"Year": "年",
"Planner": "计划者",
"Meal_Type_Required": "用餐类型是必需的",
"AddFoodToShopping": "添加 {food} 到购物清单",
"RemoveFoodFromShopping": "从购物清单中移除 {food}",
"IgnoredFood": "已忽略购买 {food}。",
"Add_Servings_to_Shopping": "添加 {servings} 份到购物",
"Inherit": "继承",
"InheritFields": "继承字段值",
"FoodInherit": "食物可继承的字段",
"ShowUncategorizedFood": "显示未定义",
"GroupBy": "分组",
"MoveCategory": "移动到: ",
"IgnoreThis": "永不自动添加 {food} 到购物",
"DelayFor": "延迟 {hours} 小时",
"Warning": "警告",
"NoCategory": "未选择分类。",
"Completed": "完成",
"OfflineAlert": "您处于离线状态,购物清单可能无法同步。",
"shopping_share": "分享购物清单",
"shopping_auto_sync": "自动同步",
"mealplan_autoadd_shopping": "自动添加用餐计划",
"mealplan_autoinclude_related": "添加相关的菜谱",
"default_delay": "默认延迟时间",
"mealplan_autoadd_shopping_desc": "自动将用餐计划配料添加到购物清单中。",
"mealplan_autoexclude_onhand_desc": "将用餐计划添加到购物清单时(手动或自动),排除当前手头上的配料。",
"mealplan_autoinclude_related_desc": "将用餐计划(手动或自动)添加到购物清单时,包括所有相关菜谱。",
"default_delay_desc": "延迟购物清单条目的默认小时数。",
"err_move_self": "无法将项目移动到自身",
"nothing": "无事可做",
"show_sql": "显示 SQL",
"filter_to_supermarket_desc": "默认情况下,过滤购物清单只包括所选超市的类别。",
"shopping_recent_days_desc": "显示最近几天的购物清单条目。",
"shopping_recent_days": "最近几天",
"create_shopping_new": "添加到新的购物清单",
"download_pdf": "下载 PDF",
"download_csv": "下载 CSV",
"csv_delim_help": "用于 CSV 导出的分隔符。",
"csv_delim_label": "CSV 分隔符",
"SuccessClipboard": "购物清单已复制到剪贴板",
"copy_to_clipboard": "复制到剪贴板",
"csv_prefix_label": "清单前缀",
"copy_markdown_table": "复制为 Markdown 表格",
"in_shopping": "在购物清单上",
"DelayUntil": "推迟到",
"mark_complete": "标记完成",
"QuickEntry": "快速入口",
"shopping_add_onhand_desc": "在核对购物清单时,将食物标记为“入手”。",
"shopping_add_onhand": "自动入手",
"related_recipes": "相关的菜谱",
"today_recipes": "今日菜谱",
"sql_debug": "调试 SQL"
}

View File

@ -46,7 +46,7 @@ registerRoute(
registerRoute(
({request}) => (request.destination === 'script' || request.destination === 'style'),
new StaleWhileRevalidate({
new NetworkFirst({
cacheName: 'assets'
})
)

View File

@ -59,7 +59,7 @@ export class Models {
// MODELS - inherits and takes precedence over MODEL_TYPES and ACTIONS
static FOOD = {
name: i18n.t("Food"), // *OPTIONAL* : parameters will be built model -> model_type -> default
name: "Food", // *OPTIONAL* : parameters will be built model -> model_type -> default
apiName: "Food", // *REQUIRED* : the name that is used in api.ts for this model
model_type: this.TREE, // *OPTIONAL* : model specific params for api, if not present will attempt modeltype_create then default_create
paginated: true,
@ -76,15 +76,17 @@ export class Models {
// REQUIRED: unordered array of fields that can be set during create
create: {
// if not defined partialUpdate will use the same parameters, prepending 'id'
params: [["name", "description", "recipe", "food_onhand", "supermarket_category", "inherit", "inherit_fields"]],
params: [["name", "description", "recipe", "food_onhand", "supermarket_category", "inherit", "inherit_fields", "ignore_shopping"]],
form: {
show_help: true,
name: {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
placeholder: "",
subtitle_field: "full_name",
},
description: {
form_field: true,
@ -99,12 +101,21 @@ export class Models {
field: "recipe",
list: "RECIPE",
label: i18n.t("Recipe"),
help_text: i18n.t("food_recipe_help"),
},
shopping: {
onhand: {
form_field: true,
type: "checkbox",
field: "food_onhand",
label: i18n.t("OnHand"),
help_text: i18n.t("OnHand_help"),
},
ignore_shopping: {
form_field: true,
type: "checkbox",
field: "ignore_shopping",
label: i18n.t("Ignore_Shopping"),
help_text: i18n.t("ignore_shopping_help"),
},
shopping_category: {
form_field: true,
@ -113,6 +124,7 @@ export class Models {
list: "SHOPPING_CATEGORY",
label: i18n.t("Shopping_Category"),
allow_create: true,
help_text: i18n.t("shopping_category_help"),
},
inherit_fields: {
form_field: true,
@ -121,12 +133,7 @@ export class Models {
field: "inherit_fields",
list: "FOOD_INHERIT_FIELDS",
label: i18n.t("InheritFields"),
condition: { field: "parent", value: true, condition: "exists" },
},
full_name: {
form_field: true,
type: "smalltext",
field: "full_name",
condition: { field: "food_children_exist", value: true, condition: "preference_equals" },
},
form_function: "FoodCreateDefault",
},
@ -136,12 +143,12 @@ export class Models {
},
}
static FOOD_INHERIT_FIELDS = {
name: i18n.t("FoodInherit"),
name: "FoodInherit",
apiName: "FoodInheritField",
}
static KEYWORD = {
name: i18n.t("Keyword"), // *OPTIONAL: parameters will be built model -> model_type -> default
name: "Keyword", // *OPTIONAL: parameters will be built model -> model_type -> default
apiName: "Keyword",
model_type: this.TREE,
paginated: true,
@ -184,7 +191,7 @@ export class Models {
}
static UNIT = {
name: i18n.t("Unit"),
name: "Unit",
apiName: "Unit",
paginated: true,
create: {
@ -210,7 +217,7 @@ export class Models {
}
static SHOPPING_LIST = {
name: i18n.t("Shopping_list"),
name: "Shopping_list",
apiName: "ShoppingListEntry",
list: {
params: ["id", "checked", "supermarket", "options"],
@ -239,7 +246,7 @@ export class Models {
}
static RECIPE_BOOK = {
name: i18n.t("Recipe_Book"),
name: "Recipe_Book",
apiName: "RecipeBook",
create: {
params: [["name", "description", "icon"]],
@ -269,7 +276,7 @@ export class Models {
}
static SHOPPING_CATEGORY = {
name: i18n.t("Shopping_Category"),
name: "Shopping_Category",
apiName: "SupermarketCategory",
create: {
params: [["name", "description"]],
@ -293,7 +300,7 @@ export class Models {
}
static SHOPPING_CATEGORY_RELATION = {
name: i18n.t("Shopping_Category_Relation"),
name: "Shopping_Category_Relation",
apiName: "SupermarketCategoryRelation",
create: {
params: [["category", "supermarket", "order"]],
@ -317,7 +324,7 @@ export class Models {
}
static SUPERMARKET = {
name: i18n.t("Supermarket"),
name: "Supermarket",
apiName: "Supermarket",
ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }],
create: {
@ -360,7 +367,7 @@ export class Models {
}
static AUTOMATION = {
name: i18n.t("Automation"),
name: "Automation",
apiName: "Automation",
paginated: true,
list: {
@ -423,7 +430,7 @@ export class Models {
}
static RECIPE = {
name: i18n.t("Recipe"),
name: "Recipe",
apiName: "Recipe",
list: {
params: ["query", "keywords", "foods", "units", "rating", "books", "keywordsOr", "foodsOr", "booksOr", "internal", "random", "_new", "page", "pageSize", "options"],
@ -439,7 +446,7 @@ export class Models {
}
static USER_NAME = {
name: i18n.t("User"),
name: "User",
apiName: "User",
list: {
params: ["filter_list"],
@ -447,7 +454,7 @@ export class Models {
}
static MEAL_TYPE = {
name: i18n.t("Meal_Type"),
name: "Meal_Type",
apiName: "MealType",
list: {
params: ["filter_list"],
@ -455,7 +462,7 @@ export class Models {
}
static MEAL_PLAN = {
name: i18n.t("Meal_Plan"),
name: "Meal_Plan",
apiName: "MealPlan",
list: {
params: ["options"],
@ -463,7 +470,7 @@ export class Models {
}
static USERFILE = {
name: i18n.t("File"),
name: "File",
apiName: "UserFile",
paginated: false,
list: {
@ -492,13 +499,13 @@ export class Models {
},
}
static USER = {
name: i18n.t("User"),
name: "User",
apiName: "User",
paginated: false,
}
static STEP = {
name: i18n.t("Step"),
name: "Step",
apiName: "Step",
list: {
params: ["recipe", "query", "page", "pageSize", "options"],

View File

@ -312,6 +312,12 @@ export interface Food {
* @memberof Food
*/
full_name?: string;
/**
*
* @type {boolean}
* @memberof Food
*/
ignore_shopping?: boolean;
}
/**
*
@ -708,6 +714,12 @@ export interface IngredientFood {
* @memberof IngredientFood
*/
full_name?: string;
/**
*
* @type {boolean}
* @memberof IngredientFood
*/
ignore_shopping?: boolean;
}
/**
*
@ -1353,6 +1365,12 @@ export interface MealPlanRecipe {
* @memberof MealPlanRecipe
*/
_new?: string;
/**
*
* @type {string}
* @memberof MealPlanRecipe
*/
recent?: string;
}
/**
*
@ -1813,25 +1831,25 @@ export interface RecipeNutrition {
* @type {string}
* @memberof RecipeNutrition
*/
carbohydrates?: string;
carbohydrates: string;
/**
*
* @type {string}
* @memberof RecipeNutrition
*/
fats?: string;
fats: string;
/**
*
* @type {string}
* @memberof RecipeNutrition
*/
proteins?: string;
proteins: string;
/**
*
* @type {string}
* @memberof RecipeNutrition
*/
calories?: string;
calories: string;
/**
*
* @type {string}
@ -1941,6 +1959,12 @@ export interface RecipeOverview {
* @memberof RecipeOverview
*/
_new?: string;
/**
*
* @type {string}
* @memberof RecipeOverview
*/
recent?: string;
}
/**
*
@ -2016,12 +2040,6 @@ export interface RecipeSteps {
* @memberof RecipeSteps
*/
name?: string;
/**
*
* @type {string}
* @memberof RecipeSteps
*/
type?: RecipeStepsTypeEnum;
/**
*
* @type {string}
@ -2089,18 +2107,6 @@ export interface RecipeSteps {
*/
numrecipe?: string;
}
/**
* @export
* @enum {string}
*/
export enum RecipeStepsTypeEnum {
Text = 'TEXT',
Time = 'TIME',
File = 'FILE',
Recipe = 'RECIPE'
}
/**
*
* @export
@ -2621,12 +2627,6 @@ export interface Step {
* @memberof Step
*/
name?: string;
/**
*
* @type {string}
* @memberof Step
*/
type?: StepTypeEnum;
/**
*
* @type {string}
@ -2694,18 +2694,6 @@ export interface Step {
*/
numrecipe?: string;
}
/**
* @export
* @enum {string}
*/
export enum StepTypeEnum {
Text = 'TEXT',
Time = 'TIME',
File = 'FILE',
Recipe = 'RECIPE'
}
/**
*
* @export
@ -3050,6 +3038,12 @@ export interface UserPreference {
* @memberof UserPreference
*/
default_page?: UserPreferenceDefaultPageEnum;
/**
*
* @type {boolean}
* @memberof UserPreference
*/
use_fractions?: boolean;
/**
*
* @type {boolean}
@ -3124,10 +3118,10 @@ export interface UserPreference {
mealplan_autoexclude_onhand?: boolean;
/**
*
* @type {Array<number>}
* @type {Array<MealPlanShared>}
* @memberof UserPreference
*/
shopping_share?: Array<number>;
shopping_share?: Array<MealPlanShared> | null;
/**
*
* @type {number}
@ -3158,6 +3152,13 @@ export interface UserPreference {
* @memberof UserPreference
*/
shopping_add_onhand?: boolean;
/**
*
* @type {boolean}
* @memberof UserPreference
*/
left_handed?: boolean;
}
/**

View File

@ -156,7 +156,7 @@ export function getUserPreference(pref = undefined) {
return undefined
}
if (pref) {
return user_preference[pref]
return user_preference?.[pref]
}
return user_preference
}
@ -389,6 +389,8 @@ export function getForm(model, action, item1, item2) {
}
if (value?.form_field) {
value["value"] = item1?.[value?.field] ?? undefined
value["help"] = item1?.[value?.help_text_field] ?? value?.help_text ?? undefined
value["subtitle"] = item1?.[value?.subtitle_field] ?? value?.subtitle ?? undefined
form.fields.push({
...value,
...{
@ -508,32 +510,48 @@ const specialCases = {
// delete, update or change all of the category/relations
let id = result.id
let existing_categories = result.category_to_supermarket
let updated_categories = options.category_to_supermarket
let updated_categories = options.category_to_supermarket.map((x) => {
return {
...x,
category: {
id: x?.category?.id ?? x.id,
name: x?.category?.name ?? x.name,
},
id: x?.category_to_supermarket__id,
order: x?.order ?? x?.category_to_supermarket__order,
}
})
let promises = []
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
if (updated_categories?.[0]?.category?.name) {
// list of category relationship ids that are not part of the updated supermarket
let removed_categories = existing_categories.filter((x) => !updated_categories.map((x) => x.category.id).includes(x.category.id))
let added_categories = updated_categories.filter((x) => !existing_categories.map((x) => x.category.id).includes(x.category.id))
let changed_categories = updated_categories.filter((x) => existing_categories.map((x) => x.category.id).includes(x.category.id))
removed_categories.forEach((x) => {
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, { id: x.id }))
})
let item = { supermarket: id }
added_categories.forEach((x) => {
item.order = x.order
item.category = { id: x.category.id, name: x.category.name }
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
})
changed_categories.forEach((x) => {
item.id = x?.id ?? existing_categories.find((y) => y.category.id === x.category.id).id
item.order = x.order
item.category = { id: x.category.id, name: x.category.name }
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
})
}
// list of category relationship ids that are not part of the updated supermarket
let removed_categories = existing_categories.filter((x) => !updated_categories.map((x) => x.category.id).includes(x.category.id))
let added_categories = updated_categories.filter((x) => !existing_categories.map((x) => x.category.id).includes(x.category.id))
let changed_categories = updated_categories.filter((x) => existing_categories.map((x) => x.category.id).includes(x.category.id))
let order = Math.max(...existing_categories.map((x) => x?.order ?? 0), ...updated_categories.map((x) => x?.order ?? 0), 0) + 1
removed_categories.forEach((x) => {
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, { id: x.id }))
})
let item = { supermarket: id }
added_categories.forEach((x) => {
item.order = x?.order ?? order
if (!x?.order) {
order = order + 1
}
item.category = { id: x.category.id, name: x.category.name }
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
})
changed_categories.forEach((x) => {
item.id = x?.id ?? existing_categories.find((y) => y.category.id === x.category.id).id
item.order = x?.order ?? order
if (!x?.order) {
order = order + 1
}
item.category = { id: x.category.id, name: x.category.name }
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
})
return Promise.all(promises).then(() => {
// finally get and return the Supermarket which everything downstream is expecting