Skip to content

Commit

Permalink
lots of shopping improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
vabene1111 committed Oct 20, 2024
1 parent 615e3bf commit 2ed53f8
Show file tree
Hide file tree
Showing 31 changed files with 179 additions and 251 deletions.
270 changes: 70 additions & 200 deletions vue3/src/components/display/ShoppingLineItem.vue
Original file line number Diff line number Diff line change
@@ -1,141 +1,52 @@
<template>
<div class="swipe-container" :id="itemContainerId" @touchend="handleSwipe()"
v-if="(useUserPreferenceStore().deviceSettings.shopping_show_checked_entries || !isChecked) && (useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries || !isDelayed)"
<v-list-item class="swipe-container" :id="itemContainerId" @touchend="handleSwipe()"
v-if="(useUserPreferenceStore().deviceSettings.shopping_show_checked_entries || !isChecked) && (useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries || !isDelayed)"
@click="detail_modal_visible = true"
>
<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="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>-->

<v-btn-group class="swipe-element">
<template #prepend>
<v-btn color="primary" v-if="isDelayed">
<i class="fa-fw fas fa-hourglass-half"></i>
</v-btn>
<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 && !isChecked"></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>
</template>

<div class="flex-grow-1 p-2">
<div class="d-flex">
<div class="d-flex flex-column pr-2">
<span v-for="[i, a] in amounts" v-bind:key="a">

<span>
<i class="fas fa-check" v-if="a.checked && !isChecked"></i>
<i class="fas fa-hourglass-half" v-if="a.delayed && !a.checked"></i> <b>
{{ a.amount }}
{{ a.unit.name }} </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-disabled">{{ info_row }}</small></span>
</div>


</div>
</div>

<template #append>
<v-btn color="success" @click="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true)"
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}">
<i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" icon="fa-solid fa-check" variant="plain">
</v-btn>
</v-btn-group>
<div class="swipe-action bg-primary justify-content-end">
<i class="fa-fw fas fa-hourglass-half swipe-icon"></i>
</div>
<!-- <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>

<v-dialog v-model="detail_modal_visible">
<v-card>
<v-card-title>
<h5> {{ food_row }}</h5>
<small class="text-muted">{{ food.description }}</small>

</v-card-title>
<v-card-text>

<h5 class="mt-2">{{ $t('Quick actions') }}</h5>
{{ $t('Category') }}
<v-select
class="form-control mb-2"
:items="useShoppingStore().supermarketCategories"
item-title="name"
item-value="id"
return-object
v-model="food.supermarket_category"
@input="detail_modal_visible = false; updateFoodCategory(food)"
></v-select>

<v-btn color="info" block
@click="detail_modal_visible = false;useShoppingStore().delayEntries(entries,!isDelayed, true)">
{{ $t('Postpone') }}
</v-btn>


<h6 class="mt-2">{{ $t('Entries') }}</h6>


<v-row v-for="e in entries" v-bind:key="e.id">
<v-col cold="12">

<v-btn-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 !== ''">
<!-- TOOD used to be a link to view_recipe -->
<a> <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 }}
{{ DateTime().fromJSDate(e.recipe_mealplan.mealplan_from_date).toLocaleString(DateTime.DATETIME_SHORT)}}
<br/>
</span>

{{ e.created_by.display_name }} {{ DateTime().fromJSDate(e.created_at).toLocaleString(DateTime.DATETIME_SHORT) }}<br/>
</small>
</span>

</div>
<v-btn color="error"
@click="useShoppingStore().deleteObject(e)"><i
class="fas fa-trash"></i></v-btn>
</v-btn-group>

<!-- TODO implement -->
<!-- <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"/>
</v-col>

</v-row>

<v-btn color="success" block @click="useShoppingListStore().createObject({ amount: 0, unit: null, food: food, })"> {{ $t("Add") }}</v-btn>
<v-btn color="warning" block @click="detail_modal_visible = false; setFoodIgnoredAndChecked(food)"> {{ $t("Ignore_Shopping") }}</v-btn>
<v-btn color="danger" block class="mt-2"
@click="detail_modal_visible = false;useShoppingListStore().deleteEntries(entries)">
{{ $t('Delete_All') }}
</v-btn>

</v-card-text>
</v-card>


</v-dialog>

<!-- <generic-modal-form :model="Models.FOOD" :show="editing_food !== null"-->
<!-- @hidden="editing_food = null; useShoppingListStore().refreshFromAPI()"></generic-modal-form>-->

</div>
</template>

<script setup lang="ts">
Expand All @@ -147,6 +58,7 @@ 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 {ShoppingLineAmount} from "@/types/Shopping";
const props = defineProps({
entries: {type: [] as PropType<ShoppingListEntry[]>, required: true},
Expand Down Expand Up @@ -187,40 +99,42 @@ const food = computed(() => {
return props.entries[Object.keys(props.entries)[0]]['food']
})

const amounts = computed(() => {
let unit_amounts = {}
/**
* 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((): Map<number, ShoppingLineAmount> => {
let unitAmounts = new Map<number, ShoppingLineAmount>()
for (let i in props.entries) {
let e = props.entries[i]
if (!e.checked && e.delayUntil === null
if (!e.checked && (e.delayUntil == null)
|| (e.checked && useUserPreferenceStore().deviceSettings.shopping_show_checked_entries)
|| (e.delayUntil !== null && useUserPreferenceStore().deviceSettings.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.delayUntil !== null)}
} else {
unit_amounts[unit] = {id: e.unit.id, unit: e.unit.name, amount: e.amount, checked: e.checked, delayed: (e.delayUntil !== null)}
}
if (unitAmounts.get(unit) != undefined) {
unitAmounts.get(unit)!.amount += e.amount
} else {
unitAmounts.set(unit, {
amount: e.amount,
unit: e.unit,
checked: e.checked,
delayed: (e.delayUntil != null)
} as ShoppingLineAmount)
}
}
}
}
return unit_amounts
})

const food_row = computed(() => {
return food.value.name
return unitAmounts
})
const info_row = computed(() => {
Expand All @@ -237,7 +151,6 @@ const info_row = computed(() => {
authors.push(e.createdBy.displayName)
}

if (e.recipeMealplan !== null) {
let recipe_name = e.recipeMealplan.recipeName
if (recipes.indexOf(recipe_name) === -1) {
Expand Down Expand Up @@ -307,63 +220,20 @@ function setFoodIgnoredAndChecked(food: Food) {
* 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)
}
//
// 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>
/* 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;
}

.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>
/* TODO swipe system classes removed because not working (visually, touch detection was working), retrieve from old ShoppingLineItem VCS */
</style>
27 changes: 19 additions & 8 deletions vue3/src/components/display/ShoppingListView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<v-tabs v-model="currentTab" grow>
<v-tabs v-model="currentTab" density="compact">
<v-tab value="shopping"><i class="fas fa-shopping-cart fa-fw"></i> <span class="d-none d-md-block ms-1">{{ $t('Shopping_list') }}</span></v-tab>
<v-tab value="recipes"><i class="fas fa-book fa-fw"></i> <span class="d-none d-md-block ms-1">{{ $t('Recipes') }}</span></v-tab>
</v-tabs>
Expand All @@ -9,24 +9,31 @@
<v-container>
<v-row>
<v-col>
<v-text-field :label="$t('Shopping_input_placeholder')" @keyup.enter="addIngredient()" v-model="ingredientInput">
<v-text-field :label="$t('Shopping_input_placeholder')" density="compact" @keyup.enter="addIngredient()" v-model="ingredientInput" hide-details>
<template #append>
<v-btn
density="comfortable"
@click="addIngredient()"
:icon="ingredientInputIcon"
color="create"
></v-btn>
</template>
</v-text-field>

<v-list lines="two" density="compact">
<v-list class="mt-3" density="compact">
<template v-for="category in useShoppingStore().getEntriesByGroup" :key="category.name">
<template v-if="(category.stats.countUnchecked > 0 || useUserPreferenceStore().deviceSettings.shopping_show_checked_entries)
&& (category.stats.countUnchecked + category.stats.countChecked) > 0
&& (category.stats.countUncheckedDelayed < category.stats.countUnchecked || useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries)">

<template v-for="category in useShoppingStore().getEntriesByGroup">
<v-list-subheader>{{ category.name }}</v-list-subheader>
<v-divider></v-divider>
<v-list-subheader v-if="category.name === useShoppingStore().UNDEFINED_CATEGORY"><i>{{ $t('NoCategory') }}</i></v-list-subheader>
<v-list-subheader v-else>{{ category.name }}</v-list-subheader>
<v-divider></v-divider>

<template v-for="[i, value] in category.foods" :key="i">
<shopping-line-item :entries="Array.from(value.entries.values())"></shopping-line-item>
</template>

<template v-for="[i, value] in category.foods" :key="i">
<shopping-line-item :entries="Array.from(value.entries.values())"></shopping-line-item>
</template>
</template>
</v-list>
Expand All @@ -48,6 +55,7 @@ import {useShoppingStore} from "@/stores/ShoppingStore";
import {ApiApi, Food, IngredientString, ShoppingListEntry, Unit} from "@/openapi";
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
const currentTab = ref("shopping")
Expand All @@ -58,6 +66,9 @@ onMounted(() => {
useShoppingStore().refreshFromAPI()
})
/**
* add new ingredient from ingredient text input
*/
function addIngredient() {
const api = new ApiApi()
Expand Down
Loading

0 comments on commit 2ed53f8

Please sign in to comment.