331 lines
12 KiB
Vue
331 lines
12 KiB
Vue
<template>
|
|
<div id="shopping_line_item" class="swipe-container" @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-1" @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">{{ a.amount }} {{ a.unit }}<br/></span>
|
|
</div>
|
|
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
|
{{ food.name }}
|
|
</div>
|
|
</div>
|
|
|
|
<span v-if="info_row"><small class="text-muted">{{ info_row }}</small></span>
|
|
</div>
|
|
<b-button variant="success" @click="useShoppingListStore().setEntriesCheckedState(entries, !is_checked)"
|
|
:class="{'btn-success': !is_checked, 'btn-warning': is_checked}">
|
|
<i class="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">
|
|
<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>
|
|
|
|
<!-- TODO implement -->
|
|
<!-- <b-button variant="success" block @click="detail_modal_visible = false;"> {{ $t("Edit_Food") }}</b-button> -->
|
|
|
|
<b-button variant="info" block
|
|
@click="detail_modal_visible = false;useShoppingListStore().delayEntries(entries,!this.is_delayed, true)">
|
|
{{ $t('Postpone') }}
|
|
</b-button>
|
|
|
|
|
|
<h6 class="mt-2">{{ $t('Entries') }}</h6>
|
|
|
|
<b-button variant="danger" block
|
|
@click="detail_modal_visible = false;useShoppingListStore().deleteEntries(entries)">
|
|
{{ $t('Delete_All') }}
|
|
</b-button>
|
|
|
|
<b-row v-for="e in entries" v-bind:key="e.id">
|
|
<b-col cold="12">
|
|
<b-button-group class="mt-1 w-100">
|
|
<b-button variant="dark" block class="btn btn-block text-left">
|
|
<span><span v-if="e.amount > 0">{{ e.amount }}</span> {{ e.unit?.name }} {{ food.name }}</span>
|
|
<span><br/><small class="text-muted">
|
|
<span v-if="e.recipe_mealplan && e.recipe_mealplan.recipe_name !== ''">
|
|
<a :href="resolveDjangoUrl('view_recipe', e.recipe_mealplan.recipe)"> {{
|
|
e.recipe_mealplan.recipe_name
|
|
}} </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>
|
|
|
|
</b-button>
|
|
<b-button variant="warning"
|
|
@click="detail_modal_visible = false; useShoppingListStore().deleteObject(e)"><i
|
|
class="fas fa-trash"></i></b-button> <!-- TODO implement -->
|
|
</b-button-group>
|
|
|
|
<number-scaler-component :number="e.amount"
|
|
@change="e.amount = $event; useShoppingListStore().updateObject(e)"
|
|
v-if="e.recipe_mealplan === null"></number-scaler-component>
|
|
|
|
</b-col>
|
|
</b-row>
|
|
</template>
|
|
|
|
<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>
|
|
|
|
<script>
|
|
import Vue from "vue"
|
|
import {BootstrapVue} from "bootstrap-vue"
|
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
|
import {ApiMixin, 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";
|
|
|
|
|
|
Vue.use(BootstrapVue)
|
|
|
|
export default {
|
|
name: "ShoppingLineItem",
|
|
mixins: [ApiMixin],
|
|
components: {GenericModalForm, NumberScalerComponent},
|
|
props: {
|
|
entries: {type: Object,},
|
|
},
|
|
data() {
|
|
return {
|
|
detail_modal_visible: false,
|
|
editing_food: null,
|
|
}
|
|
},
|
|
computed: {
|
|
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]
|
|
|
|
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}
|
|
} else {
|
|
unit_amounts[unit] = {id: e.unit.id, unit: e.unit.name, amount: e.amount}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
return unit_amounts
|
|
},
|
|
food_row: function () {
|
|
return this.food.name
|
|
},
|
|
info_row: function () {
|
|
let info_row = []
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (useUserPreferenceStore().device_settings.shopping_item_info_created_by && authors.length > 0) {
|
|
info_row.push(authors.join(', '))
|
|
}
|
|
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() {
|
|
|
|
},
|
|
methods: {
|
|
useUserPreferenceStore,
|
|
useShoppingListStore,
|
|
resolveDjangoUrl,
|
|
|
|
formatDate: function (datetime) {
|
|
if (!datetime) {
|
|
return
|
|
}
|
|
return Intl.DateTimeFormat(window.navigator.language, {
|
|
dateStyle: "short",
|
|
}).format(Date.parse(datetime))
|
|
},
|
|
/**
|
|
* 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]
|
|
}
|
|
|
|
let apiClient = new ApiApiFactory()
|
|
apiClient.updateFood(food.id, food).then(r => {
|
|
|
|
}).catch((err) => {
|
|
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
|
|
})
|
|
},
|
|
/**
|
|
* 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('.swipe-container');
|
|
// get the distance the user swiped
|
|
const swipeDistance = container.scrollLeft - container.clientWidth;
|
|
if (swipeDistance < minDistance * -1) {
|
|
useShoppingListStore().setEntriesCheckedState(this.entries, !this.is_checked)
|
|
} else if (swipeDistance > minDistance) {
|
|
useShoppingListStore().delayEntries(this.entries, !this.is_delayed, true)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
</script>
|
|
|
|
|
|
<style>
|
|
/* scroll snap takes care of restoring scroll position */
|
|
.swipe-container {
|
|
display: flex;
|
|
overflow: auto;
|
|
overflow-x: scroll;
|
|
scroll-snap-type: x mandatory;
|
|
}
|
|
|
|
/* scrollbar should be hidden */
|
|
.swipe-container::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
/* 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>
|