Merge branch 'feature/shopping-ui' into develop
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,45 @@
|
||||
|
||||
<div id="app">
|
||||
<div>
|
||||
<markdown-editor-component></markdown-editor-component>
|
||||
|
||||
<div class="swipe-container">
|
||||
<div class="swipe-action bg-success">
|
||||
<i class="swipe-icon fa-fw fas fa-check"></i>
|
||||
</div>
|
||||
|
||||
<b-button-group class="swipe-element">
|
||||
|
||||
<div class="card flex-grow-1 btn-block p-2">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column pr-2">
|
||||
<span>
|
||||
<span><i class="fas fa-check"></i> <b>100 g </b></span>
|
||||
<br/>
|
||||
</span>
|
||||
<span>
|
||||
<span><i class="fas fa-check"></i> <b>200 kg </b></span>
|
||||
<br/>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||
Erdbeeren <br/>
|
||||
<span><small class="text-muted">vabene111</small></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<b-button variant="success">
|
||||
<i class="d-print-none fa-fw fas fa-check"></i>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
<div class="swipe-action bg-primary justify-content-end">
|
||||
<i class="fa-fw fas fa-hourglass-half swipe-icon"></i>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -30,7 +67,7 @@ Vue.use(BootstrapVue)
|
||||
export default {
|
||||
name: "TestView",
|
||||
mixins: [ApiMixin],
|
||||
components: {MarkdownEditorComponent},
|
||||
components: {},
|
||||
computed: {},
|
||||
data() {
|
||||
return {}
|
||||
@ -49,5 +86,46 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.swipe-container {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
/* scrollbar should be hidden */
|
||||
.swipe-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.swipe-container {
|
||||
scrollbar-width: none; /* For Firefox */
|
||||
}
|
||||
|
||||
/* main element should always snap into view */
|
||||
.swipe-element {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.swipe-icon {
|
||||
color: white;
|
||||
position: sticky;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
/* swipe-actions and element should be 100% wide */
|
||||
.swipe-action,
|
||||
.swipe-element {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.swipe-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
@ -11,14 +11,14 @@
|
||||
<div class="flex-column" v-if="show_button_1">
|
||||
<slot name="button_1">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_search' }" v-bind:href="resolveDjangoUrl('view_search')">
|
||||
<i class="fas fa-fw fa-book " style="font-size: 1.5em"></i><br/><small>{{ $t('Recipes') }}</small></a> <!-- TODO localize -->
|
||||
<i class="fas fa-fw fa-book " style="font-size: 1.4em"></i><br/><small>{{ $t('Recipes') }}</small></a> <!-- TODO localize -->
|
||||
</slot>
|
||||
|
||||
</div>
|
||||
<div class="flex-column" v-if="show_button_2">
|
||||
<slot name="button_2">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_plan' }" v-bind:href="resolveDjangoUrl('view_plan')">
|
||||
<i class="fas fa-calendar-alt" style="font-size: 1.5em"></i><br/><small>{{ $t('Meal_Plan') }}</small></a>
|
||||
<i class="fas fa-calendar-alt" style="font-size: 1.4em"></i><br/><small>{{ $t('Meal_Plan') }}</small></a>
|
||||
</slot>
|
||||
|
||||
</div>
|
||||
@ -53,14 +53,14 @@
|
||||
<div class="flex-column" v-if="show_button_3">
|
||||
<slot name="button_3">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_shopping' }" v-bind:href="resolveDjangoUrl('view_shopping')">
|
||||
<i class="fas fa-shopping-cart" style="font-size: 1.5em"></i><br/><small>{{ $t('Shopping_list') }}</small></a>
|
||||
<i class="fas fa-shopping-cart" style="font-size: 1.4em"></i><br/><small>{{ $t('Shopping_list') }}</small></a>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="flex-column">
|
||||
|
||||
<slot name="button_4" v-if="show_button_4">
|
||||
<a class="nav-link bottom-nav-link p-0" v-bind:class="{'bottom-nav-link-active': activeView === 'view_books' }" v-bind:href="resolveDjangoUrl('view_books')">
|
||||
<i class="fas fa-book-open" style="font-size: 1.5em"></i><br/><small>{{ $t('Books') }}</small></a> <!-- TODO localize -->
|
||||
<i class="fas fa-book-open" style="font-size: 1.4em"></i><br/><small>{{ $t('Books') }}</small></a> <!-- TODO localize -->
|
||||
</slot>
|
||||
|
||||
</div>
|
||||
|
@ -6,7 +6,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import html2pdf from "html2pdf.js"
|
||||
|
||||
export default {
|
||||
name: "DownloadPDF",
|
||||
@ -20,12 +19,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
downloadFile() {
|
||||
const doc = document.querySelector(this.dom)
|
||||
var options = {
|
||||
margin: 1,
|
||||
filename: this.name,
|
||||
}
|
||||
html2pdf().from(doc).set(options).save()
|
||||
window.print()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -186,8 +186,5 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.b-form-spinbutton.form-control {
|
||||
background-color: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
74
vue/src/components/NumberScalerComponent.vue
Normal file
74
vue/src/components/NumberScalerComponent.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-button-group class="w-100 mt-1">
|
||||
<b-button @click="updateNumber( 'half')" variant="outline-info"
|
||||
:disabled="disable"><i class="fas fa-divide"></i> 2
|
||||
</b-button>
|
||||
<b-button variant="outline-info" @click="updateNumber( 'sub')"
|
||||
:disabled="disable"><i class="fas fa-minus"></i>
|
||||
</b-button>
|
||||
<b-button variant="outline-info" @click="updateNumber('prompt')"
|
||||
:disabled="disable">
|
||||
{{ number }}
|
||||
</b-button>
|
||||
<b-button variant="outline-info" @click="updateNumber( 'add')"
|
||||
:disabled="disable"><i class="fas fa-plus"></i>
|
||||
</b-button>
|
||||
<b-button @click="updateNumber('multiply')" variant="outline-info"
|
||||
:disabled="disable"><i class="fas fa-times"></i> 2
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
name: "NumberScalerComponent",
|
||||
props: {
|
||||
number: {type: Number, default:0},
|
||||
disable: {type: Boolean, default: false}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* perform given operation on linked number
|
||||
* @param operation update mode
|
||||
*/
|
||||
updateNumber: function(operation) {
|
||||
if (operation === 'half') {
|
||||
this.$emit('change', this.number / 2)
|
||||
}
|
||||
if (operation === 'multiply') {
|
||||
this.$emit('change', this.number * 2)
|
||||
}
|
||||
if (operation === 'add') {
|
||||
this.$emit('change', this.number + 1)
|
||||
}
|
||||
if (operation === 'sub') {
|
||||
this.$emit('change', this.number - 1)
|
||||
}
|
||||
if (operation === 'prompt') {
|
||||
let input_number = prompt(this.$t('Input'), this.number);
|
||||
if (input_number !== null && input_number !== "" && !isNaN(input_number) && !isNaN(parseFloat(input_number))) {
|
||||
this.$emit('change', parseFloat(input_number))
|
||||
} else {
|
||||
console.log('Invalid number input in prompt', input_number)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div v-if="user_preferences !== undefined">
|
||||
<div v-if="useUserPreferenceStore().user_settings !== undefined">
|
||||
<b-form-group :label="$t('shopping_share')" :description="$t('shopping_share_desc')">
|
||||
<generic-multiselect
|
||||
@change="user_preferences.shopping_share = $event.val; updateSettings(false)"
|
||||
@change="useUserPreferenceStore().user_settings.shopping_share = $event.val; updateSettings(false)"
|
||||
:model="Models.USER"
|
||||
:initial_selection="user_preferences.shopping_share"
|
||||
:initial_selection="useUserPreferenceStore().user_settings.shopping_share"
|
||||
label="display_name"
|
||||
:multiple="true"
|
||||
:placeholder="$t('User')"
|
||||
@ -12,101 +12,98 @@
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('shopping_auto_sync')" :description="$t('shopping_auto_sync_desc')">
|
||||
<b-form-input type="range" :min="SHOPPING_MIN_AUTOSYNC_INTERVAL" max="60" step="1" v-model="user_preferences.shopping_auto_sync"
|
||||
@change="updateSettings(false)"></b-form-input>
|
||||
<b-form-input type="range" :min="SHOPPING_MIN_AUTOSYNC_INTERVAL" max="60" step="1" v-model="useUserPreferenceStore().user_settings.shopping_auto_sync"
|
||||
@change="updateSettings(false)" :disabled="useUserPreferenceStore().user_settings.shopping_auto_sync < 1"></b-form-input>
|
||||
<div class="text-center">
|
||||
<span v-if="user_preferences.shopping_auto_sync > 0">
|
||||
{{ Math.round(user_preferences.shopping_auto_sync) }}
|
||||
<span v-if="user_preferences.shopping_auto_sync === 1">{{ $t('Second') }}</span>
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_auto_sync > 0">
|
||||
{{ Math.round(useUserPreferenceStore().user_settings.shopping_auto_sync) }}
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_auto_sync === 1">{{ $t('Second') }}</span>
|
||||
<span v-else> {{ $t('Seconds') }}</span>
|
||||
</span>
|
||||
|
||||
<span v-if="user_preferences.shopping_auto_sync < 1">{{ $t('Disable') }}</span>
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_auto_sync < 1">{{ $t('Disable') }}</span>
|
||||
</div>
|
||||
<br/>
|
||||
<b-button class="btn btn-sm" @click="user_preferences.shopping_auto_sync = 0; updateSettings(false)">{{ $t('Disabled') }}</b-button>
|
||||
<b-button class="btn btn-sm" @click="useUserPreferenceStore().user_settings.shopping_auto_sync = 0; updateSettings(false)"
|
||||
v-if="useUserPreferenceStore().user_settings.shopping_auto_sync > 0">{{ $t('Disable') }}</b-button>
|
||||
<b-button class="btn btn-sm btn-success" @click="useUserPreferenceStore().user_settings.shopping_auto_sync = SHOPPING_MIN_AUTOSYNC_INTERVAL; updateSettings(false)"
|
||||
v-if="useUserPreferenceStore().user_settings.shopping_auto_sync < 1">{{ $t('Enable') }}</b-button>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('mealplan_autoadd_shopping_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.mealplan_autoadd_shopping"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.mealplan_autoadd_shopping"
|
||||
@change="updateSettings(false)">{{ $t('mealplan_autoadd_shopping') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('mealplan_autoexclude_onhand_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.mealplan_autoexclude_onhand"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.mealplan_autoexclude_onhand"
|
||||
@change="updateSettings(false)">{{ $t('mealplan_autoexclude_onhand') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('mealplan_autoinclude_related_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.mealplan_autoinclude_related"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.mealplan_autoinclude_related"
|
||||
@change="updateSettings(false)">{{ $t('mealplan_autoinclude_related') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('shopping_add_onhand_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.shopping_add_onhand"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.shopping_add_onhand"
|
||||
@change="updateSettings(false)">{{ $t('shopping_add_onhand') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('default_delay')" :description="$t('default_delay_desc')">
|
||||
<b-form-input type="range" min="1" max="72" step="1" v-model="user_preferences.default_delay"
|
||||
<b-form-input type="range" min="1" max="72" step="1" v-model="useUserPreferenceStore().user_settings.default_delay"
|
||||
@change="updateSettings(false)"></b-form-input>
|
||||
<div class="text-center">
|
||||
<span>{{ Math.round(user_preferences.default_delay) }}
|
||||
<span v-if="user_preferences.default_delay === 1">{{ $t('Hour') }}</span>
|
||||
<span>{{ Math.round(useUserPreferenceStore().user_settings.default_delay) }}
|
||||
<span v-if="useUserPreferenceStore().user_settings.default_delay === 1">{{ $t('Hour') }}</span>
|
||||
<span v-else> {{ $t('Hours') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :description="$t('filter_to_supermarket_desc')">
|
||||
<b-form-checkbox v-model="user_preferences.filter_to_supermarket"
|
||||
<b-form-checkbox v-model="useUserPreferenceStore().user_settings.filter_to_supermarket"
|
||||
@change="updateSettings(false)">{{ $t('filter_to_supermarket') }}
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('shopping_recent_days')" :description="$t('shopping_recent_days_desc')">
|
||||
<b-form-input type="range" min="0" max="14" step="1" v-model="user_preferences.shopping_recent_days"
|
||||
<b-form-input type="range" min="0" max="14" step="1" v-model="useUserPreferenceStore().user_settings.shopping_recent_days"
|
||||
@change="updateSettings(false)"></b-form-input>
|
||||
<div class="text-center">
|
||||
<span>{{ Math.round(user_preferences.shopping_recent_days) }}
|
||||
<span v-if="user_preferences.shopping_recent_days === 1">{{ $t('Day') }}</span>
|
||||
<span>{{ Math.round(useUserPreferenceStore().user_settings.shopping_recent_days) }}
|
||||
<span v-if="useUserPreferenceStore().user_settings.shopping_recent_days === 1">{{ $t('Day') }}</span>
|
||||
<span v-else> {{ $t('Days') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('csv_delim_label')" :description="$t('csv_delim_help')">
|
||||
<b-form-input v-model="user_preferences.csv_delim" @change="updateSettings(false)"></b-form-input>
|
||||
<b-form-input v-model="useUserPreferenceStore().user_settings.csv_delim" @change="updateSettings(false)"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group :label="$t('csv_prefix_label')" :description="$t('csv_prefix_help')">
|
||||
<b-form-input v-model="user_preferences.csv_prefix" @change="updateSettings(false)"></b-form-input>
|
||||
<b-form-input v-model="useUserPreferenceStore().user_settings.csv_prefix" @change="updateSettings(false)"></b-form-input>
|
||||
</b-form-group>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import {ApiMixin, StandardToasts} from "@/utils/utils";
|
||||
|
||||
import axios from "axios";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
|
||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
|
||||
export default {
|
||||
name: "ShoppingSettingsComponent",
|
||||
mixins: [ApiMixin],
|
||||
components: {GenericMultiselect},
|
||||
props: {
|
||||
user_id: Number,
|
||||
},
|
||||
props: { },
|
||||
data() {
|
||||
return {
|
||||
user_preferences: undefined,
|
||||
@ -115,30 +112,15 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.user_preferences = this.preferences
|
||||
this.languages = window.AVAILABLE_LANGUAGES
|
||||
this.loadSettings()
|
||||
|
||||
useUserPreferenceStore().loadUserSettings(false)
|
||||
},
|
||||
methods: {
|
||||
loadSettings: function () {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
|
||||
this.user_preferences = result.data
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
},
|
||||
useUserPreferenceStore,
|
||||
updateSettings: function (reload) {
|
||||
let apiFactory = new ApiApiFactory()
|
||||
this.$emit('updated', this.user_preferences)
|
||||
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||
if (reload) {
|
||||
location.reload()
|
||||
}
|
||||
}).catch(err => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
useUserPreferenceStore().updateUserSettings()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,176 +1,134 @@
|
||||
<template>
|
||||
<div id="shopping_line_item" class="pt-1">
|
||||
<b-row align-h="start"
|
||||
ref="shopping_line_item" class="invis-border">
|
||||
<b-col cols="2" 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-0 pl-1 pr-md-3 pl-md-3 dropdown-toggle-no-caret">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="swipe-container" :id="item_container_id" @touchend="handleSwipe()"
|
||||
v-if="(useUserPreferenceStore().device_settings.shopping_show_checked_entries || !is_checked) && (useUserPreferenceStore().device_settings.shopping_show_delayed_entries || !is_delayed)"
|
||||
>
|
||||
<div class="swipe-action" :class="{'bg-success': !is_checked , 'bg-warning': is_checked }">
|
||||
<i class="swipe-icon fa-fw fas" :class="{'fa-check': !is_checked , 'fa-cart-plus': is_checked }"></i>
|
||||
</div>
|
||||
|
||||
<b-button-group class="swipe-element">
|
||||
<b-button variant="primary" v-if="is_delayed">
|
||||
<i class="fa-fw fas fa-hourglass-half"></i>
|
||||
</b-button>
|
||||
<div class="card flex-grow-1 btn-block p-2" @click="detail_modal_visible = true">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column pr-2" v-if="Object.keys(amounts).length> 0">
|
||||
<span v-for="a in amounts" v-bind:key="a.id">
|
||||
|
||||
<span><i class="fas fa-check" v-if="a.checked && !is_checked"></i><i class="fas fa-hourglass-half" v-if="a.delayed && !a.checked"></i> <b>{{ a.amount }} {{
|
||||
a.unit
|
||||
}} </b></span>
|
||||
<br/></span>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||
{{ food.name }} <br/>
|
||||
<span v-if="info_row"><small class="text-muted">{{ info_row }}</small></span>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="1" class="px-1 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">
|
||||
<b-row class="d-flex h-100">
|
||||
<b-col cols="6" md="3" class="d-flex align-items-center" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
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="6" md="3" class="d-flex flex-column" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
v-if="Object.entries(formatAmount).length != 1">
|
||||
<div class="small" v-for="(x, i) in Object.entries(formatAmount)" :key="i">
|
||||
{{ x[1] }}  
|
||||
{{ x[0] }}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<b-button variant="success" @click="useShoppingListStore().setEntriesCheckedState(entries, !is_checked, true)"
|
||||
:class="{'btn-success': !is_checked, 'btn-warning': is_checked}">
|
||||
<i class="d-print-none fa-fw fas" :class="{'fa-check': !is_checked , 'fa-cart-plus': is_checked }"></i>
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
<div class="swipe-action bg-primary justify-content-end">
|
||||
<i class="fa-fw fas fa-hourglass-half swipe-icon"></i>
|
||||
</div>
|
||||
|
||||
<b-modal v-model="detail_modal_visible" @hidden="detail_modal_visible = false" body-class="pr-4 pl-4 pt-0">
|
||||
<template #modal-title>
|
||||
<h5> {{ food_row }}</h5>
|
||||
<small class="text-muted">{{ food.description }}</small>
|
||||
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<h5 class="mt-2">{{ $t('Quick actions') }}</h5>
|
||||
{{ $t('Category') }}
|
||||
<b-form-select
|
||||
class="form-control mb-2"
|
||||
:options="useShoppingListStore().supermarket_categories"
|
||||
text-field="name"
|
||||
value-field="id"
|
||||
v-model="food.supermarket_category"
|
||||
@change="detail_modal_visible = false; updateFoodCategory(food)"
|
||||
></b-form-select>
|
||||
|
||||
<b-button variant="info" block
|
||||
@click="detail_modal_visible = false;useShoppingListStore().delayEntries(entries,!is_delayed, true)">
|
||||
{{ $t('Postpone') }}
|
||||
</b-button>
|
||||
|
||||
|
||||
<h6 class="mt-2">{{ $t('Entries') }}</h6>
|
||||
|
||||
|
||||
<b-row v-for="e in entries" v-bind:key="e.id">
|
||||
<b-col cold="12">
|
||||
|
||||
<b-button-group class="w-100">
|
||||
<div class="card flex-grow-1 btn-block p-2">
|
||||
<span><i class="fas fa-check" v-if="e.checked"></i><i class="fas fa-hourglass-half" v-if="e.delay_until !== null && !e.checked"></i>
|
||||
<b><span v-if="e.amount > 0">{{ e.amount }}</span> {{ e.unit?.name }}</b> {{ food.name }}</span>
|
||||
<span><small class="text-muted">
|
||||
<span v-if="e.recipe_mealplan && e.recipe_mealplan.recipe_name !== ''">
|
||||
<a :href="resolveDjangoUrl('view_recipe', e.recipe_mealplan.recipe)"> <b> {{
|
||||
e.recipe_mealplan.recipe_name
|
||||
}} </b></a>({{
|
||||
e.recipe_mealplan.servings
|
||||
}} {{ $t('Servings') }})<br/>
|
||||
</span>
|
||||
<span
|
||||
v-if="e.recipe_mealplan && e.recipe_mealplan.mealplan_type !== undefined"> {{
|
||||
e.recipe_mealplan.mealplan_type
|
||||
}} {{ formatDate(e.recipe_mealplan.mealplan_from_date) }} <br/></span>
|
||||
|
||||
{{ e.created_by.display_name }} {{ formatDate(e.created_at) }}<br/>
|
||||
</small></span>
|
||||
|
||||
</div>
|
||||
<b-button variant="outline-danger"
|
||||
@click="useShoppingListStore().deleteObject(e)"><i
|
||||
class="fas fa-trash"></i></b-button>
|
||||
</b-button-group>
|
||||
|
||||
<generic-multiselect
|
||||
class="mt-1"
|
||||
v-if="e.recipe_mealplan === null"
|
||||
:initial_single_selection="e.unit"
|
||||
:model="Models.UNIT"
|
||||
:multiple="false"
|
||||
@change="e.unit = $event.val; useShoppingListStore().updateObject(e)"
|
||||
>
|
||||
</generic-multiselect>
|
||||
|
||||
<number-scaler-component :number="e.amount"
|
||||
@change="e.amount = $event; useShoppingListStore().updateObject(e)"
|
||||
v-if="e.recipe_mealplan === null"></number-scaler-component>
|
||||
<hr class="m-2"/>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="6" 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" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
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"><span class="ml-2">{{
|
||||
$t("Details")
|
||||
}}</span></span>
|
||||
</div>
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
<b-col cols="2" class="justify-content-start align-items-center d-flex d-md-none pl-0 pr-0" v-touch:start="startHandler" v-touch:moving="moveHandler" v-touch:end="endHandler"
|
||||
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 variant="success" block @click="useShoppingListStore().createObject({ amount: 0, unit: null, food: food, })"> {{ $t("Add") }}</b-button>
|
||||
<b-button variant="warning" block @click="detail_modal_visible = false; setFoodIgnoredAndChecked(food)"> {{ $t("Ignore_Shopping") }}</b-button>
|
||||
<b-button variant="danger" block class="mt-2"
|
||||
@click="detail_modal_visible = false;useShoppingListStore().deleteEntries(entries)">
|
||||
{{ $t('Delete_All') }}
|
||||
</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="2" md="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 pr-md-3 pl-md-3 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="7" 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 mt-1 mb-1 mt-md-3 mb-md-3" v-if="x !== entries.length - 1"/>
|
||||
<div class="pb-1 pb-md-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>
|
||||
<i class="fa fa-hourglass fa-lg" style="display: none; position: absolute" aria-hidden="true"
|
||||
ref="delay_icon"></i>
|
||||
<i class="fa fa-check fa-lg" style="display: none; position: absolute" aria-hidden="true" ref="check_icon"></i>
|
||||
|
||||
<template #modal-footer>
|
||||
<span></span>
|
||||
</template>
|
||||
</b-modal>
|
||||
|
||||
<generic-modal-form :model="Models.FOOD" :show="editing_food !== null"
|
||||
@hidden="editing_food = null; useShoppingListStore().refreshFromAPI()"></generic-modal-form>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -178,292 +136,237 @@
|
||||
import Vue from "vue"
|
||||
import {BootstrapVue} from "bootstrap-vue"
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
||||
import {ApiMixin} from "@/utils/utils"
|
||||
import RecipeCard from "./RecipeCard.vue"
|
||||
import Vue2TouchEvents from "vue2-touch-events"
|
||||
import {ApiMixin, FormatMixin, resolveDjangoUrl, StandardToasts} from "@/utils/utils"
|
||||
import {useMealPlanStore} from "@/stores/MealPlanStore";
|
||||
import {useShoppingListStore} from "@/stores/ShoppingListStore";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import NumberScalerComponent from "@/components/NumberScalerComponent.vue";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm.vue";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect.vue";
|
||||
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(Vue2TouchEvents)
|
||||
|
||||
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},
|
||||
mixins: [ApiMixin, FormatMixin],
|
||||
components: {GenericMultiselect, GenericModalForm, NumberScalerComponent},
|
||||
props: {
|
||||
entries: {
|
||||
type: Array,
|
||||
},
|
||||
settings: Object,
|
||||
groupby: {type: String},
|
||||
entries: {type: Object,},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDetails: false,
|
||||
recipe: undefined,
|
||||
servings: 1,
|
||||
dragStartX: 0,
|
||||
distance_left: 0
|
||||
detail_modal_visible: false,
|
||||
editing_food: null,
|
||||
}
|
||||
},
|
||||
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
|
||||
item_container_id: function () {
|
||||
let id = 'id_sli_'
|
||||
for (let i in this.entries) {
|
||||
id += i + '_'
|
||||
}
|
||||
return id
|
||||
},
|
||||
is_checked: function () {
|
||||
for (let i in this.entries) {
|
||||
if (!this.entries[i].checked) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
is_delayed: function () {
|
||||
for (let i in this.entries) {
|
||||
if (Date.parse(this.entries[i].delay_until) > new Date(Date.now())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
food: function () {
|
||||
return this.entries[Object.keys(this.entries)[0]]['food']
|
||||
},
|
||||
amounts: function () {
|
||||
let unit_amounts = {}
|
||||
|
||||
for (let i in this.entries) {
|
||||
let e = this.entries[i]
|
||||
|
||||
if (!e.checked && e.delay_until === null
|
||||
|| (e.checked && useUserPreferenceStore().device_settings.shopping_show_checked_entries)
|
||||
|| (e.delay_until !== null && useUserPreferenceStore().device_settings.shopping_show_delayed_entries)) {
|
||||
|
||||
let unit = -1
|
||||
if (e.unit !== undefined && e.unit !== null) {
|
||||
unit = e.unit.id
|
||||
}
|
||||
if (e.amount > 0) {
|
||||
if (unit in unit_amounts) {
|
||||
unit_amounts[unit]['amount'] += e.amount
|
||||
} else {
|
||||
if (unit === -1) {
|
||||
unit_amounts[unit] = {id: -1, unit: "", amount: e.amount, checked: e.checked, delayed: (e.delay_until !== null)}
|
||||
} else {
|
||||
unit_amounts[unit] = {id: e.unit.id, unit: e.unit.name, amount: e.amount, checked: e.checked, delayed: (e.delay_until !== null)}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
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
|
||||
return unit_amounts
|
||||
},
|
||||
formatCategory: function () {
|
||||
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
||||
food_row: function () {
|
||||
return this.food.name
|
||||
},
|
||||
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(" ")
|
||||
info_row: function () {
|
||||
let info_row = []
|
||||
|
||||
return mealplan_name
|
||||
.map((x) => {
|
||||
return this.formatOneMealPlan(x)
|
||||
})
|
||||
.join(" - ")
|
||||
let authors = []
|
||||
let recipes = []
|
||||
let meal_pans = []
|
||||
|
||||
for (let i in this.entries) {
|
||||
let e = this.entries[i]
|
||||
|
||||
if (authors.indexOf(e.created_by.display_name) === -1) {
|
||||
authors.push(e.created_by.display_name)
|
||||
}
|
||||
|
||||
|
||||
if (e.recipe_mealplan !== null) {
|
||||
let recipe_name = e.recipe_mealplan.recipe_name
|
||||
if (recipes.indexOf(recipe_name) === -1) {
|
||||
recipes.push(recipe_name.substring(0, 14) + (recipe_name.length > 14 ? '..' : ''))
|
||||
}
|
||||
|
||||
if ('mealplan_from_date' in e.recipe_mealplan) {
|
||||
let meal_plan_entry = (e?.recipe_mealplan?.mealplan_type || '') + ' (' + this.formatDate(e.recipe_mealplan.mealplan_from_date) + ')'
|
||||
if (meal_pans.indexOf(meal_plan_entry) === -1) {
|
||||
meal_pans.push(meal_plan_entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
formatNotes: function () {
|
||||
if (this.entries?.length == 1) {
|
||||
return this.formatOneNote(this.entries[0]) || ""
|
||||
|
||||
if (useUserPreferenceStore().device_settings.shopping_item_info_created_by && authors.length > 0) {
|
||||
info_row.push(authors.join(', '))
|
||||
}
|
||||
return ""
|
||||
},
|
||||
if (useUserPreferenceStore().device_settings.shopping_item_info_recipe && recipes.length > 0) {
|
||||
info_row.push(recipes.join(', '))
|
||||
}
|
||||
if (useUserPreferenceStore().device_settings.shopping_item_info_mealplan && meal_pans.length > 0) {
|
||||
info_row.push(meal_pans.join(', '))
|
||||
}
|
||||
|
||||
return info_row.join(' - ')
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
this.servings = this.entries?.[0]?.recipe_mealplan?.servings ?? 0
|
||||
|
||||
},
|
||||
methods: {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
|
||||
formatDate: function (datetime) {
|
||||
if (!datetime) {
|
||||
return
|
||||
useUserPreferenceStore,
|
||||
useShoppingListStore,
|
||||
resolveDjangoUrl,
|
||||
/**
|
||||
* update the food after the category was changed
|
||||
* handle changing category to category ID as a workaround
|
||||
* @param food
|
||||
*/
|
||||
updateFoodCategory: function (food) {
|
||||
if (typeof food.supermarket_category === "number") { // not the best solution, but as long as generic multiselect does not support caching, I don't want to use a proper model
|
||||
food.supermarket_category = this.useShoppingListStore().supermarket_categories.filter(sc => sc.id === food.supermarket_category)[0]
|
||||
}
|
||||
return Intl.DateTimeFormat(window.navigator.language, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(Date.parse(datetime))
|
||||
},
|
||||
startHandler: function (event) {
|
||||
if (event.changedTouches.length > 0) {
|
||||
this.dragStartX = event.changedTouches[0].clientX
|
||||
}
|
||||
},
|
||||
getOffset(el) {
|
||||
let rect = el.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left + window.scrollX,
|
||||
top: rect.top + window.scrollY,
|
||||
right: rect.right - window.scrollX,
|
||||
};
|
||||
},
|
||||
moveHandler: function (event) {
|
||||
let item = this.$refs['shopping_line_item'];
|
||||
this.distance_left = event.changedTouches[0].clientX - this.dragStartX;
|
||||
item.style.marginLeft = this.distance_left
|
||||
item.style.marginRight = -this.distance_left
|
||||
item.style.backgroundColor = '#ddbf86'
|
||||
item.style.border = "1px solid #000"
|
||||
|
||||
let delay_icon = this.$refs['delay_icon']
|
||||
let check_icon = this.$refs['check_icon']
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.updateFood(food.id, food).then(r => {
|
||||
|
||||
let color_factor = Math.abs(this.distance_left) / 100
|
||||
|
||||
if (this.distance_left > 0) {
|
||||
item.parentElement.parentElement.style.backgroundColor = 'rgba(130,170,139,0)'.replace(/[^,]+(?=\))/, color_factor)
|
||||
check_icon.style.display = "block"
|
||||
check_icon.style.left = this.getOffset(item.parentElement.parentElement).left + 40
|
||||
check_icon.style.top = this.getOffset(item.parentElement.parentElement).top - 92
|
||||
check_icon.style.opacity = color_factor - 0.3
|
||||
} else {
|
||||
item.parentElement.parentElement.style.backgroundColor = 'rgba(185,135,102,0)'.replace(/[^,]+(?=\))/, color_factor)
|
||||
delay_icon.style.display = "block"
|
||||
delay_icon.style.left = this.getOffset(item.parentElement.parentElement).right - 40
|
||||
delay_icon.style.top = this.getOffset(item.parentElement.parentElement).top - 92
|
||||
delay_icon.style.opacity = color_factor - 0.3
|
||||
}
|
||||
},
|
||||
endHandler: function (event) {
|
||||
let item = this.$refs['shopping_line_item'];
|
||||
item.removeAttribute('style');
|
||||
item.parentElement.parentElement.removeAttribute('style');
|
||||
|
||||
let delay_icon = this.$refs['delay_icon']
|
||||
let check_icon = this.$refs['check_icon']
|
||||
|
||||
delay_icon.style.display = "none"
|
||||
check_icon.style.display = "none"
|
||||
|
||||
if (Math.abs(this.distance_left) > window.screen.width / 6) {
|
||||
if (this.distance_left > 0) {
|
||||
let checked = false;
|
||||
this.entries.forEach((cur) => {
|
||||
checked = cur.checked
|
||||
})
|
||||
let update = {entries: this.entries.map((x) => x.id), checked: !checked}
|
||||
this.$emit("update-checkbox", update)
|
||||
} else {
|
||||
this.$emit("update-delaythis", this.entries)
|
||||
}
|
||||
}
|
||||
},
|
||||
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.display_name, "@", 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)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
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}
|
||||
}
|
||||
console.log(update)
|
||||
this.$emit("update-checkbox", update)
|
||||
/**
|
||||
* set food on_hand status to true and check all associated entries
|
||||
* @param food
|
||||
*/
|
||||
setFoodIgnoredAndChecked: function (food) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
food.ignore_shopping = true
|
||||
apiClient.updateFood(food.id, food).then(r => {
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
|
||||
useShoppingListStore().setEntriesCheckedState(this.entries, true, false)
|
||||
},
|
||||
/**
|
||||
* function triggered by touchend event of swipe container
|
||||
* check if min distance is reached and execute desired action
|
||||
*/
|
||||
handleSwipe: function () {
|
||||
const minDistance = 80;
|
||||
const container = document.querySelector('#' + this.item_container_id);
|
||||
// get the distance the user swiped
|
||||
const swipeDistance = container.scrollLeft - container.clientWidth;
|
||||
if (swipeDistance < minDistance * -1) {
|
||||
useShoppingListStore().setEntriesCheckedState(this.entries, !this.is_checked, true)
|
||||
} else if (swipeDistance > minDistance) {
|
||||
useShoppingListStore().delayEntries(this.entries, !this.is_delayed, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--style src="vue-multiselect/dist/vue-multiselect.min.css"></style-->
|
||||
|
||||
<style>
|
||||
/* table { border-collapse:collapse } /* Ensure no space between cells */
|
||||
/* tr.strikeout td { position:relative } /* Setup a new coordinate system */
|
||||
/* tr.strikeout td:before { /* Create a new element that */
|
||||
/* content: " "; /* …has no text content */
|
||||
/* position: absolute; /* …is absolutely positioned */
|
||||
/* 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;
|
||||
/* scroll snap takes care of restoring scroll position */
|
||||
.swipe-container {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.checkbox-control-mobile {
|
||||
font-size: 1rem;
|
||||
/* scrollbar should be hidden */
|
||||
.swipe-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
-moz-transition: all 0.25s linear;
|
||||
-webkit-transition: all 0.25s linear;
|
||||
transition: all 0.25s linear;
|
||||
.swipe-container {
|
||||
scrollbar-width: none; /* For Firefox */
|
||||
}
|
||||
|
||||
.rotated {
|
||||
-moz-transform: rotate(90deg);
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
/* main element should always snap into view */
|
||||
.swipe-element {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.unit-badge-lg {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 500 !important;
|
||||
.swipe-icon {
|
||||
color: white;
|
||||
position: sticky;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dropdown-spacing {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
/* swipe-actions and element should be 100% wide */
|
||||
.swipe-action,
|
||||
.swipe-element {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.invis-border {
|
||||
border: 1px solid transparent;
|
||||
.swipe-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.fa-ellipsis-v {
|
||||
font-size: 20px;
|
||||
}
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.fa-ellipsis-v {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -96,6 +96,9 @@
|
||||
"base_unit": "Base Unit",
|
||||
"base_amount": "Base Amount",
|
||||
"Datatype": "Datatype",
|
||||
"Input": "Input",
|
||||
"Undo": "Undo",
|
||||
"NoMoreUndo": "No changes to be undone.",
|
||||
"Number of Objects": "Number of Objects",
|
||||
"Add_Step": "Add Step",
|
||||
"Keywords": "Keywords",
|
||||
@ -141,6 +144,7 @@
|
||||
"Edit": "Edit",
|
||||
"Image": "Image",
|
||||
"Delete": "Delete",
|
||||
"Delete_All": "Delete all",
|
||||
"Open": "Open",
|
||||
"Ok": "Ok",
|
||||
"Save": "Save",
|
||||
@ -239,6 +243,7 @@
|
||||
"Week": "Week",
|
||||
"Month": "Month",
|
||||
"Year": "Year",
|
||||
"created_by": "Created by",
|
||||
"Planner": "Planner",
|
||||
"Planner_Settings": "Planner settings",
|
||||
"Period": "Period",
|
||||
@ -295,9 +300,11 @@
|
||||
"Warning": "Warning",
|
||||
"NoCategory": "No category selected.",
|
||||
"InheritWarning": "{food} is set to inherit, changes may not persist.",
|
||||
"ShowDelayed": "Show Delayed Items",
|
||||
"ShowDelayed": "Show delayed items",
|
||||
"ShowRecentlyCompleted": "Show recently completed items",
|
||||
"Completed": "Completed",
|
||||
"OfflineAlert": "You are offline, shopping list may not syncronize.",
|
||||
"ShoppingBackgroundSyncWarning": "Bad network, waiting to sync ...",
|
||||
"shopping_share": "Share Shopping List",
|
||||
"shopping_auto_sync": "Autosync",
|
||||
"one_url_per_line": "One URL per line",
|
||||
@ -496,6 +503,7 @@
|
||||
"Reset": "Reset",
|
||||
"Disabled": "Disabled",
|
||||
"Disable": "Disable",
|
||||
"Enable": "Enable",
|
||||
"Options": "Options",
|
||||
"Create Food": "Create Food",
|
||||
"create_food_desc": "Create a food and link it to this recipe.",
|
||||
|
491
vue/src/stores/ShoppingListStore.js
Normal file
491
vue/src/stores/ShoppingListStore.js
Normal file
@ -0,0 +1,491 @@
|
||||
import {ApiApiFactory} from "@/utils/openapi/api"
|
||||
import {StandardToasts} from "@/utils/utils"
|
||||
import {defineStore} from "pinia"
|
||||
import Vue from "vue"
|
||||
import _ from 'lodash';
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
import moment from "moment/moment";
|
||||
|
||||
const _STORE_ID = "shopping_list_store"
|
||||
/*
|
||||
* test store to play around with pinia and see if it can work for my use cases
|
||||
* don't trust that all shopping list entries are in store as there is no cache validation logic, its just a shared data holder
|
||||
* */
|
||||
export const useShoppingListStore = defineStore(_STORE_ID, {
|
||||
state: () => ({
|
||||
// shopping data
|
||||
entries: {},
|
||||
supermarket_categories: [],
|
||||
supermarkets: [],
|
||||
|
||||
total_unchecked: 0,
|
||||
total_checked: 0,
|
||||
total_unchecked_food: 0,
|
||||
total_checked_food: 0,
|
||||
|
||||
// internal
|
||||
currently_updating: false,
|
||||
last_autosync: null,
|
||||
autosync_has_focus: true,
|
||||
autosync_timeout_id: null,
|
||||
undo_stack: [],
|
||||
|
||||
queue_timeout_id: undefined,
|
||||
item_check_sync_queue: {},
|
||||
|
||||
// constants
|
||||
GROUP_CATEGORY: 'food.supermarket_category.name',
|
||||
GROUP_CREATED_BY: 'created_by.display_name',
|
||||
GROUP_RECIPE: 'recipe_mealplan.recipe_name',
|
||||
|
||||
UNDEFINED_CATEGORY: 'shopping_undefined_category'
|
||||
}),
|
||||
getters: {
|
||||
/**
|
||||
* build a multi-level data structure ready for display from shopping list entries
|
||||
* group by selected grouping key
|
||||
* @return {{}}
|
||||
*/
|
||||
get_entries_by_group: function () {
|
||||
let structure = {}
|
||||
let ordered_structure = []
|
||||
|
||||
// build structure
|
||||
for (let i in this.entries) {
|
||||
structure = this.updateEntryInStructure(structure, this.entries[i], useUserPreferenceStore().device_settings.shopping_selected_grouping)
|
||||
}
|
||||
|
||||
// statistics for UI conditions and display
|
||||
let total_unchecked = 0
|
||||
let total_checked = 0
|
||||
let total_unchecked_food = 0
|
||||
let total_checked_food = 0
|
||||
for (let i in structure) {
|
||||
let count_unchecked = 0
|
||||
let count_checked = 0
|
||||
let count_unchecked_food = 0
|
||||
let count_checked_food = 0
|
||||
|
||||
for (let fi in structure[i]['foods']) {
|
||||
let food_checked = true
|
||||
for (let ei in structure[i]['foods'][fi]['entries']) {
|
||||
if (structure[i]['foods'][fi]['entries'][ei].checked) {
|
||||
count_checked++
|
||||
} else {
|
||||
food_checked = false
|
||||
count_unchecked++
|
||||
}
|
||||
}
|
||||
if (food_checked) {
|
||||
count_checked_food++
|
||||
} else {
|
||||
count_unchecked_food++
|
||||
}
|
||||
}
|
||||
|
||||
Vue.set(structure[i], 'count_unchecked', count_unchecked)
|
||||
Vue.set(structure[i], 'count_checked', count_checked)
|
||||
Vue.set(structure[i], 'count_unchecked_food', count_unchecked_food)
|
||||
Vue.set(structure[i], 'count_checked_food', count_checked_food)
|
||||
|
||||
total_unchecked += count_unchecked
|
||||
total_checked += count_checked
|
||||
total_unchecked_food += count_unchecked_food
|
||||
total_checked_food += count_checked_food
|
||||
}
|
||||
|
||||
this.total_unchecked = total_unchecked
|
||||
this.total_checked = total_checked
|
||||
this.total_unchecked_food = total_unchecked_food
|
||||
this.total_checked_food = total_checked_food
|
||||
|
||||
// ordering
|
||||
if (this.UNDEFINED_CATEGORY in structure) {
|
||||
ordered_structure.push(structure[this.UNDEFINED_CATEGORY])
|
||||
Vue.delete(structure, this.UNDEFINED_CATEGORY)
|
||||
}
|
||||
|
||||
if (useUserPreferenceStore().device_settings.shopping_selected_grouping === this.GROUP_CATEGORY && useUserPreferenceStore().device_settings.shopping_selected_supermarket !== null) {
|
||||
for (let c of useUserPreferenceStore().device_settings.shopping_selected_supermarket.category_to_supermarket) {
|
||||
if (c.category.name in structure) {
|
||||
ordered_structure.push(structure[c.category.name])
|
||||
Vue.delete(structure, c.category.name)
|
||||
}
|
||||
}
|
||||
if (!useUserPreferenceStore().device_settings.shopping_show_selected_supermarket_only) {
|
||||
for (let i in structure) {
|
||||
ordered_structure.push(structure[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i in structure) {
|
||||
ordered_structure.push(structure[i])
|
||||
}
|
||||
}
|
||||
|
||||
return ordered_structure
|
||||
},
|
||||
/**
|
||||
* flattened list of entries used for exporters
|
||||
* kinda uncool but works for now
|
||||
* @return {*[]}
|
||||
*/
|
||||
get_flat_entries: function () {
|
||||
let items = []
|
||||
for (let i in this.get_entries_by_group) {
|
||||
for (let f in this.get_entries_by_group[i]['foods']) {
|
||||
for (let e in this.get_entries_by_group[i]['foods'][f]['entries']) {
|
||||
items.push({
|
||||
amount: this.get_entries_by_group[i]['foods'][f]['entries'][e].amount,
|
||||
unit: this.get_entries_by_group[i]['foods'][f]['entries'][e].unit?.name ?? '',
|
||||
food: this.get_entries_by_group[i]['foods'][f]['entries'][e].food?.name ?? '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
},
|
||||
/**
|
||||
* list of options available for grouping entry display
|
||||
* @return {[{id: *, translatable_label: string},{id: *, translatable_label: string},{id: *, translatable_label: string}]}
|
||||
*/
|
||||
grouping_options: function () {
|
||||
return [
|
||||
{'id': this.GROUP_CATEGORY, 'translatable_label': 'Category'},
|
||||
{'id': this.GROUP_CREATED_BY, 'translatable_label': 'created_by'},
|
||||
{'id': this.GROUP_RECIPE, 'translatable_label': 'Recipe'}
|
||||
]
|
||||
},
|
||||
/**
|
||||
* checks if failed items are contained in the sync queue
|
||||
*/
|
||||
has_failed_items: function () {
|
||||
for (let i in this.item_check_sync_queue) {
|
||||
if (this.item_check_sync_queue[i]['status'] === 'syncing_failed_before' || this.item_check_sync_queue[i]['status'] === 'waiting_failed_before') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
/**
|
||||
* Retrieves all shopping related data (shopping list entries, supermarkets, supermarket categories and shopping list recipes) from API
|
||||
*/
|
||||
refreshFromAPI() {
|
||||
if (!this.currently_updating) {
|
||||
this.currently_updating = true
|
||||
this.last_autosync = new Date().getTime();
|
||||
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listShoppingListEntrys().then((r) => {
|
||||
this.entries = {}
|
||||
|
||||
r.data.forEach((e) => {
|
||||
Vue.set(this.entries, e.id, e)
|
||||
})
|
||||
this.currently_updating = false
|
||||
}).catch((err) => {
|
||||
this.currently_updating = false
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
|
||||
apiClient.listSupermarketCategorys().then(r => {
|
||||
this.supermarket_categories = r.data
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
|
||||
apiClient.listSupermarkets().then(r => {
|
||||
this.supermarkets = r.data
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* perform auto sync request to special endpoint returning only entries changed since last auto sync
|
||||
* only updates local entries that are older than the server version
|
||||
*/
|
||||
autosync() {
|
||||
if (!this.currently_updating && this.autosync_has_focus) {
|
||||
console.log('running autosync')
|
||||
|
||||
this.currently_updating = true
|
||||
|
||||
let previous_autosync = this.last_autosync
|
||||
this.last_autosync = new Date().getTime();
|
||||
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listShoppingListEntrys(undefined, undefined, undefined, {
|
||||
'query': {'last_autosync': previous_autosync}
|
||||
}).then((r) => {
|
||||
r.data.forEach((e) => {
|
||||
// dont update stale client data
|
||||
if (!(Object.keys(this.entries).includes(e.id.toString())) || Date.parse(this.entries[e.id].updated_at) < Date.parse(e.updated_at)) {
|
||||
console.log('auto sync updating entry ', e)
|
||||
Vue.set(this.entries, e.id, e)
|
||||
}
|
||||
})
|
||||
this.currently_updating = false
|
||||
}).catch((err) => {
|
||||
console.warn('auto sync failed')
|
||||
this.currently_updating = false
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Create a new shopping list entry
|
||||
* adds new entry to store
|
||||
* @param object entry object to create
|
||||
* @return {Promise<T | void>} promise of creation call to subscribe to
|
||||
*/
|
||||
createObject(object) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
return apiClient.createShoppingListEntry(object).then((r) => {
|
||||
Vue.set(this.entries, r.data.id, r.data)
|
||||
this.registerChange('CREATED', {[r.data.id]: r.data},)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* update existing entry object and updated_at timestamp
|
||||
* updates data in store
|
||||
* IMPORTANT: always use this method to update objects to keep client state consistent
|
||||
* @param object entry object to update
|
||||
* @return {Promise<T | void>} promise of updating call to subscribe to
|
||||
*/
|
||||
updateObject(object) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
// sets the update_at timestamp on the client to prevent auto sync from overriding with older changes
|
||||
// moment().format() yields locale aware datetime without ms 2024-01-04T13:39:08.607238+01:00
|
||||
Vue.set(object, 'updated_at', moment().format())
|
||||
|
||||
return apiClient.updateShoppingListEntry(object.id, object).then((r) => {
|
||||
Vue.set(this.entries, r.data.id, r.data)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* delete shopping list entry object from DB and store
|
||||
* @param object entry object to delete
|
||||
* @return {Promise<T | void>} promise of delete call to subscribe to
|
||||
*/
|
||||
deleteObject(object) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
return apiClient.destroyShoppingListEntry(object.id).then((r) => {
|
||||
Vue.delete(this.entries, object.id)
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* returns a distinct list of recipes associated with unchecked shopping list entries
|
||||
*/
|
||||
getAssociatedRecipes: function () {
|
||||
let recipes = {}
|
||||
|
||||
for (let i in this.entries) {
|
||||
let e = this.entries[i]
|
||||
if (e.recipe_mealplan !== null) {
|
||||
Vue.set(recipes, e.recipe_mealplan.recipe, {
|
||||
'shopping_list_recipe_id': e.list_recipe,
|
||||
'recipe_id': e.recipe_mealplan.recipe,
|
||||
'recipe_name': e.recipe_mealplan.recipe_name,
|
||||
'servings': e.recipe_mealplan.servings,
|
||||
'mealplan_from_date': e.recipe_mealplan.mealplan_from_date,
|
||||
'mealplan_type': e.recipe_mealplan.mealplan_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return recipes
|
||||
},
|
||||
// convenience methods
|
||||
/**
|
||||
* function to set entry to its proper place in the data structure to perform grouping
|
||||
* @param {{}} structure datastructure
|
||||
* @param {*} entry entry to place
|
||||
* @param {*} group group to place entry into (must be of ShoppingListStore.GROUP_XXX/dot notation of entry property)
|
||||
* @returns {{}} datastructure including entry
|
||||
*/
|
||||
updateEntryInStructure(structure, entry, group) {
|
||||
let grouping_key = _.get(entry, group, this.UNDEFINED_CATEGORY)
|
||||
|
||||
if (grouping_key === undefined || grouping_key === null) {
|
||||
grouping_key = this.UNDEFINED_CATEGORY
|
||||
}
|
||||
|
||||
if (!(grouping_key in structure)) {
|
||||
Vue.set(structure, grouping_key, {'name': grouping_key, 'foods': {}})
|
||||
}
|
||||
if (!(entry.food.id in structure[grouping_key]['foods'])) {
|
||||
Vue.set(structure[grouping_key]['foods'], entry.food.id, {
|
||||
'id': entry.food.id,
|
||||
'name': entry.food.name,
|
||||
'entries': {}
|
||||
})
|
||||
}
|
||||
Vue.set(structure[grouping_key]['foods'][entry.food.id]['entries'], entry.id, entry)
|
||||
return structure
|
||||
},
|
||||
/**
|
||||
* function to handle user checking or unchecking a set of entries
|
||||
* @param {{}} entries set of entries
|
||||
* @param checked boolean to set checked state of entry to
|
||||
* @param undo if the user should be able to undo the change or not
|
||||
*/
|
||||
setEntriesCheckedState(entries, checked, undo) {
|
||||
if (undo) {
|
||||
this.registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
|
||||
}
|
||||
|
||||
let entry_id_list = []
|
||||
for (let i in entries) {
|
||||
Vue.set(this.entries[i], 'checked', checked)
|
||||
Vue.set(this.entries[i], 'updated_at', moment().format())
|
||||
entry_id_list.push(i)
|
||||
}
|
||||
|
||||
this.item_check_sync_queue
|
||||
Vue.set(this.item_check_sync_queue, Math.random(), {
|
||||
'ids': entry_id_list,
|
||||
'checked': checked,
|
||||
'status': 'waiting'
|
||||
})
|
||||
this.runSyncQueue(5)
|
||||
},
|
||||
/**
|
||||
* go through the list of queued requests and try to run them
|
||||
* add request back to queue if it fails due to offline or timeout
|
||||
* Do NOT call this method directly, always call using runSyncQueue method to prevent simultaneous runs
|
||||
* @private
|
||||
*/
|
||||
_replaySyncQueue() {
|
||||
if (navigator.onLine || document.location.href.includes('localhost')) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let promises = []
|
||||
|
||||
for (let i in this.item_check_sync_queue) {
|
||||
let entry = this.item_check_sync_queue[i]
|
||||
Vue.set(entry, 'status', ((entry['status'] === 'waiting') ? 'syncing' : 'syncing_failed_before'))
|
||||
Vue.set(this.item_check_sync_queue, i, entry)
|
||||
|
||||
let p = apiClient.bulkShoppingListEntry(entry, {timeout: 15000}).then((r) => {
|
||||
Vue.delete(this.item_check_sync_queue, i)
|
||||
}).catch((err) => {
|
||||
if (err.code === "ERR_NETWORK" || err.code === "ECONNABORTED") {
|
||||
Vue.set(entry, 'status', 'waiting_failed_before')
|
||||
Vue.set(this.item_check_sync_queue, i, entry)
|
||||
} else {
|
||||
Vue.delete(this.item_check_sync_queue, i)
|
||||
console.error('Failed API call for entry ', entry)
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
}
|
||||
})
|
||||
promises.push(p)
|
||||
}
|
||||
|
||||
Promise.allSettled(promises).finally(r => {
|
||||
this.runSyncQueue(500)
|
||||
})
|
||||
} else {
|
||||
this.runSyncQueue(5000)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* manages running the replaySyncQueue function after the given timeout
|
||||
* calling this function might cancel a previously created timeout
|
||||
* @param timeout time in ms after which to run the replaySyncQueue function
|
||||
*/
|
||||
runSyncQueue(timeout) {
|
||||
clearTimeout(this.queue_timeout_id)
|
||||
|
||||
this.queue_timeout_id = setTimeout(() => {
|
||||
this._replaySyncQueue()
|
||||
}, timeout)
|
||||
},
|
||||
/**
|
||||
* function to handle user "delaying" and "undelaying" shopping entries
|
||||
* @param {{}} entries set of entries
|
||||
* @param delay if entries should be delayed or if delay should be removed
|
||||
* @param undo if the user should be able to undo the change or not
|
||||
*/
|
||||
delayEntries(entries, delay, undo) {
|
||||
let delay_hours = useUserPreferenceStore().user_settings.default_delay
|
||||
let delay_date = new Date(Date.now() + delay_hours * (60 * 60 * 1000))
|
||||
|
||||
if (undo) {
|
||||
this.registerChange((delay ? 'DELAY' : 'UNDELAY'), entries)
|
||||
}
|
||||
|
||||
for (let i in entries) {
|
||||
this.entries[i].delay_until = (delay ? delay_date : null)
|
||||
this.updateObject(this.entries[i])
|
||||
}
|
||||
},
|
||||
/**
|
||||
* delete list of entries
|
||||
* @param {{}} entries set of entries
|
||||
*/
|
||||
deleteEntries(entries) {
|
||||
for (let i in entries) {
|
||||
this.deleteObject(this.entries[i])
|
||||
}
|
||||
},
|
||||
deleteShoppingListRecipe(shopping_list_recipe_id) {
|
||||
let api = new ApiApiFactory()
|
||||
|
||||
for (let i in this.entries) {
|
||||
if (this.entries[i].list_recipe === shopping_list_recipe_id) {
|
||||
Vue.delete(this.entries, i)
|
||||
}
|
||||
}
|
||||
|
||||
api.destroyShoppingListRecipe(shopping_list_recipe_id).then((x) => {
|
||||
// no need to update anything, entries were already removed
|
||||
}).catch((err) => {
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* register the change to a set of entries to allow undoing it
|
||||
* throws an Error if the operation type is not known
|
||||
* @param type the type of change to register. This determines what undoing the change does. (CREATE->delete object,
|
||||
* CHECKED->uncheck entry, UNCHECKED->check entry, DELAY->remove delay)
|
||||
* @param {{}} entries set of entries
|
||||
*/
|
||||
registerChange(type, entries) {
|
||||
if (!['CREATED', 'CHECKED', 'UNCHECKED', 'DELAY', 'UNDELAY'].includes(type)) {
|
||||
throw Error('Tried to register unknown change type')
|
||||
}
|
||||
this.undo_stack.push({'type': type, 'entries': entries})
|
||||
},
|
||||
/**
|
||||
* takes the last item from the undo stack and reverts it
|
||||
*/
|
||||
undoChange() {
|
||||
let last_item = this.undo_stack.pop()
|
||||
if (last_item !== undefined) {
|
||||
let type = last_item['type']
|
||||
let entries = last_item['entries']
|
||||
|
||||
if (type === 'CHECKED' || type === 'UNCHECKED') {
|
||||
this.setEntriesCheckedState(entries, (type === 'UNCHECKED'), false)
|
||||
} else if (type === 'DELAY' || type === 'UNDELAY') {
|
||||
this.delayEntries(entries, (type === 'UNDELAY'), false)
|
||||
} else if (type === 'CREATED') {
|
||||
for (let i in entries) {
|
||||
let e = entries[i]
|
||||
this.deleteObject(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// can use localization in store
|
||||
//StandardToasts.makeStandardToast(this, this.$t('NoMoreUndo'))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
@ -1,20 +1,136 @@
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import {ApiApiFactory, UserPreference} from "@/utils/openapi/api";
|
||||
import Vue from "vue";
|
||||
import {StandardToasts} from "@/utils/utils";
|
||||
|
||||
const _STALE_TIME_IN_MS = 1000 * 30
|
||||
const _STORE_ID = 'user_preference_store'
|
||||
|
||||
const _LS_DEVICE_SETTINGS = 'TANDOOR_LOCAL_SETTINGS'
|
||||
const _LS_USER_SETTINGS = 'TANDOOR_USER_SETTINGS'
|
||||
const _USER_ID = localStorage.getItem('USER_ID')
|
||||
|
||||
export const useUserPreferenceStore = defineStore(_STORE_ID, {
|
||||
state: () => ({
|
||||
data: null,
|
||||
updated_at: null,
|
||||
currently_updating: false,
|
||||
}),
|
||||
getters: {
|
||||
|
||||
},
|
||||
user_settings_loaded_at: new Date(0),
|
||||
user_settings: {
|
||||
image: null,
|
||||
theme: "TANDOOR",
|
||||
nav_bg_color: "#ddbf86",
|
||||
nav_text_color: "DARK",
|
||||
nav_show_logo: true,
|
||||
default_unit: "g",
|
||||
default_page: "SEARCH",
|
||||
use_fractions: false,
|
||||
use_kj: false,
|
||||
plan_share: [],
|
||||
nav_sticky: true,
|
||||
ingredient_decimals: 2,
|
||||
comments: true,
|
||||
shopping_auto_sync: 5,
|
||||
mealplan_autoadd_shopping: false,
|
||||
food_inherit_default: [],
|
||||
default_delay: "4.0000",
|
||||
mealplan_autoinclude_related: true,
|
||||
mealplan_autoexclude_onhand: true,
|
||||
shopping_share: [],
|
||||
shopping_recent_days: 7,
|
||||
csv_delim: ",",
|
||||
csv_prefix: "",
|
||||
filter_to_supermarket: false,
|
||||
shopping_add_onhand: false,
|
||||
left_handed: false,
|
||||
show_step_ingredients: true,
|
||||
food_children_exist: false,
|
||||
locally_updated_at: new Date(0),
|
||||
},
|
||||
|
||||
device_settings_initialized: false,
|
||||
device_settings_loaded_at: new Date(0),
|
||||
device_settings: {
|
||||
// shopping
|
||||
shopping_show_checked_entries: false,
|
||||
shopping_show_delayed_entries: false,
|
||||
shopping_show_selected_supermarket_only: false,
|
||||
shopping_selected_grouping: 'food.supermarket_category.name',
|
||||
shopping_selected_supermarket: null,
|
||||
shopping_item_info_created_by: false,
|
||||
shopping_item_info_mealplan: false,
|
||||
shopping_item_info_recipe: true,
|
||||
},
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// Device settings (on device settings stored in local storage)
|
||||
/**
|
||||
* Load device settings from local storage and update state device_settings
|
||||
*/
|
||||
loadDeviceSettings() {
|
||||
let s = localStorage.getItem(_LS_DEVICE_SETTINGS)
|
||||
if (!(s === null || s === {})) {
|
||||
let settings = JSON.parse(s)
|
||||
for (s in settings) {
|
||||
Vue.set(this.device_settings, s, settings[s])
|
||||
}
|
||||
}
|
||||
this.device_settings_initialized = true
|
||||
},
|
||||
/**
|
||||
* persist changes to device settings into local storage
|
||||
*/
|
||||
updateDeviceSettings: function () {
|
||||
localStorage.setItem(_LS_DEVICE_SETTINGS, JSON.stringify(this.device_settings))
|
||||
},
|
||||
// ---------------- new methods for user settings
|
||||
loadUserSettings: function (allow_cached_results) {
|
||||
let s = localStorage.getItem(_LS_USER_SETTINGS)
|
||||
if (!(s === null || s === {})) {
|
||||
let settings = JSON.parse(s)
|
||||
for (s in settings) {
|
||||
Vue.set(this.user_settings, s, settings[s])
|
||||
}
|
||||
console.log(`loaded local user settings age ${((new Date().getTime()) - this.user_settings.locally_updated_at) / 1000} `)
|
||||
}
|
||||
if (((new Date().getTime()) - this.user_settings.locally_updated_at) > _STALE_TIME_IN_MS || !allow_cached_results) {
|
||||
console.log('refreshing user settings from API')
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.retrieveUserPreference(localStorage.getItem('USER_ID')).then(r => {
|
||||
for (s in r.data) {
|
||||
if (!(s in this.user_settings) && s !== 'user') {
|
||||
// dont load new keys if no default exists (to prevent forgetting to add defaults)
|
||||
console.error(`API returned UserPreference key "${s}" which has no default in UserPreferenceStore.user_settings.`)
|
||||
} else {
|
||||
Vue.set(this.user_settings, s, r.data[s])
|
||||
}
|
||||
}
|
||||
Vue.set(this.user_settings, 'locally_updated_at', new Date().getTime())
|
||||
localStorage.setItem(_LS_USER_SETTINGS, JSON.stringify(this.user_settings))
|
||||
}).catch(err => {
|
||||
this.currently_updating = false
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
updateUserSettings: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.partialUpdateUserPreference(_USER_ID, this.user_settings).then(r => {
|
||||
this.user_settings = r.data
|
||||
Vue.set(this.user_settings, 'locally_updated_at', new Date().getTime())
|
||||
localStorage.setItem(_LS_USER_SETTINGS, JSON.stringify(this.user_settings))
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
|
||||
}).catch(err => {
|
||||
this.currently_updating = false
|
||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
||||
})
|
||||
},
|
||||
// ----------------
|
||||
// User Preferences (database settings stored in user preference model)
|
||||
/**
|
||||
* gets data from the store either directly or refreshes from API if data is considered stale
|
||||
* @returns {UserPreference|*|Promise<axios.AxiosResponse<UserPreference>>}
|
||||
@ -69,12 +185,16 @@ export const useUserPreferenceStore = defineStore(_STORE_ID, {
|
||||
*/
|
||||
refreshFromAPI() {
|
||||
let apiClient = new ApiApiFactory()
|
||||
if(!this.currently_updating){
|
||||
if (!this.currently_updating) {
|
||||
this.currently_updating = true
|
||||
return apiClient.retrieveUserPreference(localStorage.getItem('USER_ID')).then(r => {
|
||||
this.data = r.data
|
||||
this.updated_at = new Date()
|
||||
this.currently_updating = false
|
||||
|
||||
this.user_settings = r.data
|
||||
this.user_settings_loaded_at = new Date()
|
||||
|
||||
return this.data
|
||||
}).catch(err => {
|
||||
this.currently_updating = false
|
||||
|
@ -408,6 +408,7 @@ export class Models {
|
||||
static SHOPPING_CATEGORY = {
|
||||
name: "Shopping_Category",
|
||||
apiName: "SupermarketCategory",
|
||||
merge: true,
|
||||
create: {
|
||||
params: [["name", "description"]],
|
||||
form: {
|
||||
|
@ -4013,18 +4013,6 @@ export interface ShoppingListEntries {
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
unit?: FoodPropertiesFoodUnit | null;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
ingredient?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
ingredient_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4061,6 +4049,12 @@ export interface ShoppingListEntries {
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
created_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
updated_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4104,18 +4098,6 @@ export interface ShoppingListEntry {
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
unit?: FoodPropertiesFoodUnit | null;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
ingredient?: number | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
ingredient_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4152,6 +4134,12 @@ export interface ShoppingListEntry {
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
created_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
updated_at?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@ -4165,6 +4153,25 @@ export interface ShoppingListEntry {
|
||||
*/
|
||||
delay_until?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ShoppingListEntryBulk
|
||||
*/
|
||||
export interface ShoppingListEntryBulk {
|
||||
/**
|
||||
*
|
||||
* @type {Array<any>}
|
||||
* @memberof ShoppingListEntryBulk
|
||||
*/
|
||||
ids: Array<any>;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ShoppingListEntryBulk
|
||||
*/
|
||||
checked: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -4213,6 +4220,18 @@ export interface ShoppingListRecipe {
|
||||
* @memberof ShoppingListRecipe
|
||||
*/
|
||||
mealplan_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipe
|
||||
*/
|
||||
mealplan_from_date?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipe
|
||||
*/
|
||||
mealplan_type?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -4262,6 +4281,18 @@ export interface ShoppingListRecipeMealplan {
|
||||
* @memberof ShoppingListRecipeMealplan
|
||||
*/
|
||||
mealplan_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipeMealplan
|
||||
*/
|
||||
mealplan_from_date?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipeMealplan
|
||||
*/
|
||||
mealplan_type?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -4311,6 +4342,18 @@ export interface ShoppingListRecipes {
|
||||
* @memberof ShoppingListRecipes
|
||||
*/
|
||||
mealplan_note?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipes
|
||||
*/
|
||||
mealplan_from_date?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ShoppingListRecipes
|
||||
*/
|
||||
mealplan_type?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -4507,19 +4550,97 @@ export interface Space {
|
||||
* @memberof Space
|
||||
*/
|
||||
nav_logo?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Space
|
||||
*/
|
||||
space_theme?: SpaceSpaceThemeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
space_theme?: RecipeFile | null;
|
||||
custom_space_theme?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @type {string}
|
||||
* @memberof Space
|
||||
*/
|
||||
use_plural?: boolean;
|
||||
nav_bg_color?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Space
|
||||
*/
|
||||
nav_text_color?: SpaceNavTextColorEnum;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_32?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_128?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_144?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_180?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_192?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_512?: RecipeFile | null;
|
||||
/**
|
||||
*
|
||||
* @type {RecipeFile}
|
||||
* @memberof Space
|
||||
*/
|
||||
logo_color_svg?: RecipeFile | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum SpaceSpaceThemeEnum {
|
||||
Blank = 'BLANK',
|
||||
Tandoor = 'TANDOOR',
|
||||
Bootstrap = 'BOOTSTRAP',
|
||||
Darkly = 'DARKLY',
|
||||
Flatly = 'FLATLY',
|
||||
Superhero = 'SUPERHERO',
|
||||
TandoorDark = 'TANDOOR_DARK'
|
||||
}
|
||||
/**
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum SpaceNavTextColorEnum {
|
||||
Blank = 'BLANK',
|
||||
Light = 'LIGHT',
|
||||
Dark = 'DARK'
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -5382,6 +5503,39 @@ export interface ViewLog {
|
||||
*/
|
||||
export const ApiApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
bulkShoppingListEntry: async (shoppingListEntryBulk?: ShoppingListEntryBulk, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/shopping-list-entry/bulk/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(shoppingListEntryBulk, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -9486,10 +9640,11 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSupermarkets: async (options: any = {}): Promise<RequestArgs> => {
|
||||
listSupermarkets: async (query?: string, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/supermarket/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@ -9502,6 +9657,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
if (query !== undefined) {
|
||||
localVarQueryParameter['query'] = query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
@ -9661,10 +9820,11 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listUserFiles: async (options: any = {}): Promise<RequestArgs> => {
|
||||
listUserFiles: async (query?: string, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/user-file/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@ -9677,6 +9837,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
if (query !== undefined) {
|
||||
localVarQueryParameter['query'] = query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
@ -9935,6 +10099,47 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
mergeSupermarketCategory: async (id: string, target: string, supermarketCategory?: SupermarketCategory, options: any = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('mergeSupermarketCategory', 'id', id)
|
||||
// verify required parameter 'target' is not null or undefined
|
||||
assertParamExists('mergeSupermarketCategory', 'target', target)
|
||||
const localVarPath = `/api/supermarket-category/{id}/merge/{target}/`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
|
||||
.replace(`{${"target"}}`, encodeURIComponent(String(target)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(supermarketCategory, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
@ -14820,6 +15025,16 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
export const ApiApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ApiApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async bulkShoppingListEntry(shoppingListEntryBulk?: ShoppingListEntryBulk, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ShoppingListEntryBulk>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.bulkShoppingListEntry(shoppingListEntryBulk, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -16034,11 +16249,12 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listSupermarkets(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Supermarket>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarkets(options);
|
||||
async listSupermarkets(query?: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Supermarket>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarkets(query, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@ -16085,11 +16301,12 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listUserFiles(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserFile>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listUserFiles(options);
|
||||
async listUserFiles(query?: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserFile>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listUserFiles(query, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@ -16165,6 +16382,18 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergeKeyword(id, target, keyword, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async mergeSupermarketCategory(id: string, target: string, supermarketCategory?: SupermarketCategory, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SupermarketCategory>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergeSupermarketCategory(id, target, supermarketCategory, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
@ -17623,6 +17852,15 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
export const ApiApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ApiApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
bulkShoppingListEntry(shoppingListEntryBulk?: ShoppingListEntryBulk, options?: any): AxiosPromise<ShoppingListEntryBulk> {
|
||||
return localVarFp.bulkShoppingListEntry(shoppingListEntryBulk, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -18719,11 +18957,12 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSupermarkets(options?: any): AxiosPromise<Array<Supermarket>> {
|
||||
return localVarFp.listSupermarkets(options).then((request) => request(axios, basePath));
|
||||
listSupermarkets(query?: string, options?: any): AxiosPromise<Array<Supermarket>> {
|
||||
return localVarFp.listSupermarkets(query, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@ -18765,11 +19004,12 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listUserFiles(options?: any): AxiosPromise<Array<UserFile>> {
|
||||
return localVarFp.listUserFiles(options).then((request) => request(axios, basePath));
|
||||
listUserFiles(query?: string, options?: any): AxiosPromise<Array<UserFile>> {
|
||||
return localVarFp.listUserFiles(query, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@ -18837,6 +19077,17 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
mergeKeyword(id: string, target: string, keyword?: Keyword, options?: any): AxiosPromise<Keyword> {
|
||||
return localVarFp.mergeKeyword(id, target, keyword, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
mergeSupermarketCategory(id: string, target: string, supermarketCategory?: SupermarketCategory, options?: any): AxiosPromise<SupermarketCategory> {
|
||||
return localVarFp.mergeSupermarketCategory(id, target, supermarketCategory, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
@ -20160,6 +20411,17 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ApiApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {ShoppingListEntryBulk} [shoppingListEntryBulk]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public bulkShoppingListEntry(shoppingListEntryBulk?: ShoppingListEntryBulk, options?: any) {
|
||||
return ApiApiFp(this.configuration).bulkShoppingListEntry(shoppingListEntryBulk, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AccessToken} [accessToken]
|
||||
@ -21492,12 +21754,13 @@ export class ApiApi extends BaseAPI {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against supermarket name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public listSupermarkets(options?: any) {
|
||||
return ApiApiFp(this.configuration).listSupermarkets(options).then((request) => request(this.axios, this.basePath));
|
||||
public listSupermarkets(query?: string, options?: any) {
|
||||
return ApiApiFp(this.configuration).listSupermarkets(query, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21548,12 +21811,13 @@ export class ApiApi extends BaseAPI {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched against user-file name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public listUserFiles(options?: any) {
|
||||
return ApiApiFp(this.configuration).listUserFiles(options).then((request) => request(this.axios, this.basePath));
|
||||
public listUserFiles(query?: string, options?: any) {
|
||||
return ApiApiFp(this.configuration).listUserFiles(query, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21636,6 +21900,19 @@ export class ApiApi extends BaseAPI {
|
||||
return ApiApiFp(this.configuration).mergeKeyword(id, target, keyword, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this supermarket category.
|
||||
* @param {string} target
|
||||
* @param {SupermarketCategory} [supermarketCategory]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public mergeSupermarketCategory(id: string, target: string, supermarketCategory?: SupermarketCategory, options?: any) {
|
||||
return ApiApiFp(this.configuration).mergeSupermarketCategory(id, target, supermarketCategory, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id A unique integer value identifying this unit.
|
||||
|
@ -131,7 +131,7 @@ export class StandardToasts {
|
||||
}
|
||||
|
||||
|
||||
let DEBUG = localStorage.getItem("DEBUG") === "True" || always_show_errors
|
||||
let DEBUG = (localStorage.getItem("DEBUG") === "True" || always_show_errors) && variant !== 'success'
|
||||
if (DEBUG){
|
||||
console.log('ERROR ', err, JSON.stringify(err?.response?.data))
|
||||
console.trace();
|
||||
@ -365,6 +365,23 @@ export function energyHeading() {
|
||||
}
|
||||
}
|
||||
|
||||
export const FormatMixin = {
|
||||
name: "FormatMixin",
|
||||
methods: {
|
||||
/**
|
||||
* format short date from datetime
|
||||
* @param datetime any string that can be parsed by Date.parse()
|
||||
* @return {string}
|
||||
*/
|
||||
formatDate: function (datetime) {
|
||||
return Intl.DateTimeFormat(window.navigator.language, {
|
||||
dateStyle: "short",
|
||||
}).format(Date.parse(datetime))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
axios.defaults.xsrfCookieName = "csrftoken"
|
||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||
|
||||
@ -376,7 +393,7 @@ export const ApiMixin = {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// if passing parameters that are not part of the offical schema of the endpoint use parameter: options: {query: {simple: 1}}
|
||||
// if passing parameters that are not part of the official schema of the endpoint use parameter: options: {query: {simple: 1}}
|
||||
genericAPI: function (model, action, options) {
|
||||
let setup = getConfig(model, action)
|
||||
if (setup?.config?.function) {
|
||||
|
Reference in New Issue
Block a user