Files
recipes/vue3/src/components/display/ShoppingLineItem.vue
2025-06-06 15:42:39 +02:00

247 lines
8.6 KiB
Vue

<template>
<v-list-item class="swipe-container" :id="itemContainerId" @touchend="handleSwipe()"
v-if="isShoppingListFoodVisible(props.shoppingListFood, useUserPreferenceStore().deviceSettings)"
>
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">-->
<!-- <i class="swipe-icon fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
<!-- </div>-->
<div class="flex-grow-1 p-2" @click="dialog = true;">
<div class="d-flex">
<div class="d-flex flex-column pr-2">
<span v-for="a in amounts" v-bind:key="a.key">
<span>
<i class="fas fa-check text-success fa-fw" v-if="a.checked"></i>
<i class="fas fa-clock-rotate-left text-info fa-fw" v-if="a.delayed"></i> <b>
<span :class="{'text-disabled': a.checked || a.delayed}" class="text-no-wrap">
{{ $n(a.amount) }}
<span v-if="a.unit">{{ a.unit.name }}</span>
</span>
</b>
</span>
<br/>
</span>
</div>
<div class="d-flex flex-column flex-grow-1 align-self-center">
{{ shoppingListFood.food.name }} <br/>
<span v-if="infoRow"><small class="text-disabled">{{ infoRow }}</small></span>
</div>
</div>
</div>
<template v-slot:[checkBtnSlot]>
<v-btn color="success" @click.native.stop="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true);"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" :icon="actionButtonIcon" variant="plain">
</v-btn>
<!-- <i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
</template>
<!-- <div class="swipe-action bg-primary justify-content-end">-->
<!-- <i class="fa-fw fas fa-hourglass-half swipe-icon"></i>-->
<!-- </div>-->
</v-list-item>
<shopping-line-item-dialog v-model="dialog" v-model:shopping-list-food="props.shoppingListFood"></shopping-line-item-dialog>
</template>
<script setup lang="ts">
import {computed, PropType, ref} from "vue";
import {DateTime} from "luxon";
import {useShoppingStore} from "@/stores/ShoppingStore.js";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.js";
import {ApiApi, Food, ShoppingListEntry} from '@/openapi'
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping";
import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils";
import ShoppingLineItemDialog from "@/components/dialogs/ShoppingLineItemDialog.vue";
const emit = defineEmits(['clicked'])
const props = defineProps({
shoppingListFood: {type: {} as PropType<IShoppingListFood>, required: true},
hideInfoRow: {type: Boolean, default: false}
})
const checkBtnSlot = ref(useUserPreferenceStore().userSettings.leftHanded ? 'prepend' : 'append')
const dialog = ref(false)
const entries = computed(() => {
return Array.from(props.shoppingListFood.entries.values())
})
/**
* ID of outer container, used by swipe system
*/
const itemContainerId = computed(() => {
let id = 'id_sli_'
for (let i in entries.value) {
id += i + '_'
}
return id
})
/**
* tests if all entries of the given food are checked
*/
const isChecked = computed(() => {
for (let i in entries.value) {
if (!entries.value[i].checked) {
return false
}
}
return true
})
/**
* style action button depending on if all items are checked or not
*/
const actionButtonIcon = computed(() => {
if (isChecked.value) {
return 'fa-solid fa-plus'
}
return 'fa-solid fa-check'
})
/**
* calculate the amounts for the given line
* can combine 1 to n entries with the same unit
* can contain more 0 to n different entries for different units
*/
const amounts = computed((): ShoppingLineAmount[] => {
let unitAmounts: ShoppingLineAmount[] = []
for (let i in entries.value) {
let e = entries.value[i]
if (isEntryVisible(e, useUserPreferenceStore().deviceSettings)) {
let unit = -1
if (e.unit !== undefined && e.unit !== null) {
unit = e.unit.id!
}
if (e.amount > 0) {
let uaMerged = false
unitAmounts.forEach(ua => {
if (((ua.unit == null && e.unit == null) || (ua.unit != null && ua.unit.id! == unit)) && ua.checked == e.checked && ua.delayed == isDelayed(e)) {
ua.amount += e.amount
uaMerged = true
}
})
if (!uaMerged) {
unitAmounts.push({
key: `${unit}_${e.checked}_${isDelayed(e)}`,
amount: e.amount,
unit: e.unit,
checked: e.checked,
delayed: isDelayed(e)
} as ShoppingLineAmount)
}
}
}
}
return unitAmounts
})
/**
* compute the second (info) row of the line item based on the entries and the device settings
*/
const infoRow = computed(() => {
if (props.hideInfoRow) {
return ''
}
let info_row = []
let authors = []
let recipes = []
let meal_pans = []
for (let i in entries.value) {
let e = entries.value[i]
if (isEntryVisible(e, useUserPreferenceStore().deviceSettings)) {
if (authors.indexOf(e.createdBy.displayName) === -1) {
authors.push(e.createdBy.displayName)
}
if (e.listRecipe != null) {
if (e.listRecipeData.recipe != null) {
let recipe_name = e.listRecipeData.recipeData.name
if (recipes.indexOf(recipe_name) === -1) {
recipes.push(recipe_name.substring(0, 14) + (recipe_name.length > 14 ? '..' : ''))
}
}
if (e.listRecipeData.mealplan != null) {
let meal_plan_entry = (e.listRecipeData.mealPlanData.mealType.name.substring(0, 8) || '') + (e.listRecipeData.mealPlanData.mealType.name.length > 8 ? '..' : '') + ' (' + DateTime.fromJSDate(e.listRecipeData.mealPlanData.fromDate).toLocaleString(DateTime.DATE_SHORT) + ')'
if (meal_pans.indexOf(meal_plan_entry) === -1) {
meal_pans.push(meal_plan_entry)
}
}
}
}
}
if (useUserPreferenceStore().deviceSettings.shopping_item_info_created_by && authors.length > 0) {
info_row.push(authors.join(', '))
}
if (useUserPreferenceStore().deviceSettings.shopping_item_info_recipe && recipes.length > 0) {
info_row.push(recipes.join(', '))
}
if (useUserPreferenceStore().deviceSettings.shopping_item_info_mealplan && meal_pans.length > 0) {
info_row.push(meal_pans.join(', '))
}
return info_row.join(' - ')
})
/**
* set food on_hand status to true and check all associated entries
* @param food
*/
function setFoodIgnoredAndChecked(food: Food) {
let api = new ApiApi()
food.ignoreShopping = true
api.apiFoodUpdate({id: food.id!, food: food}).then(r => {
}).catch((err) => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
useShoppingStore().setEntriesCheckedState(entries.value, true, false)
}
/**
* function triggered by touchend event of swipe container
* check if min distance is reached and execute desired action
*/
function handleSwipe() {
//
// const minDistance = 80;
// const container = document.querySelector('#' + itemContainerId.value);
// // get the distance the user swiped
// const swipeDistance = container!.scrollLeft - container!.clientWidth;
// if (swipeDistance < minDistance * -1) {
// useShoppingStore().setEntriesCheckedState(props.entries, !isChecked.value, true)
// } else if (swipeDistance > minDistance) {
// useShoppingStore().delayEntries(props.entries, !isDelayed.value, true)
// }
}
</script>
<style>
/* TODO swipe system classes removed because not working (visually, touch detection was working), retrieve from old ShoppingLineItem VCS */
</style>