update shopping performance

This commit is contained in:
vabene1111
2025-12-02 17:19:35 +01:00
parent 5608f80246
commit 9dfc9e1020
3 changed files with 136 additions and 143 deletions

View File

@@ -1,6 +1,6 @@
<template> <template>
<v-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0 shopping-border" :id="itemContainerId" @touchend="handleSwipe()" @click="dialog = true;" <v-list-item class="swipe-container border-t-sm mt-0 mb-0 pt-0 pb-0 pe-0 pa-0 shopping-border" :id="itemContainerId" @touchend="handleSwipe()" @click="dialog = true;"
v-if="isShoppingListFoodVisible(props.shoppingListFood, useUserPreferenceStore().deviceSettings)"
> >
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">--> <!-- <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>--> <!-- <i class="swipe-icon fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
@@ -10,8 +10,8 @@
<span :style="{background: sl.color}" v-for="sl in shoppingList"></span> <span :style="{background: sl.color}" v-for="sl in shoppingList"></span>
</div> </div>
<div class="flex-grow-1 p-2" > <div class="flex-grow-1 p-2">
<div class="d-flex" > <div class="d-flex">
<div class="d-flex flex-column pr-2 pl-4"> <div class="d-flex flex-column pr-2 pl-4">
<span v-for="a in amounts" v-bind:key="a.key"> <span v-for="a in amounts" v-bind:key="a.key">
<span> <span>
@@ -59,7 +59,7 @@ import {computed, PropType, ref} from "vue";
import {DateTime} from "luxon"; import {DateTime} from "luxon";
import {useShoppingStore} from "@/stores/ShoppingStore.js"; import {useShoppingStore} from "@/stores/ShoppingStore.js";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.js"; import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.js";
import {ApiApi, Food, ShoppingListEntry} from '@/openapi' import {ApiApi, Food, ShoppingList, ShoppingListEntry} from '@/openapi'
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore"; import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping"; import {IShoppingListFood, ShoppingLineAmount} from "@/types/Shopping";
import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils"; import {isDelayed, isEntryVisible, isShoppingListFoodDelayed, isShoppingListFoodVisible} from "@/utils/logic_utils";
@@ -86,9 +86,7 @@ const entries = computed(() => {
*/ */
const itemContainerId = computed(() => { const itemContainerId = computed(() => {
let id = 'id_sli_' let id = 'id_sli_'
for (let i in entries.value) { entries.value.forEach(e => id += e.id + '_')
id += i + '_'
}
return id return id
}) })
@@ -117,13 +115,18 @@ const actionButtonIcon = computed(() => {
const shoppingList = computed(() => { const shoppingList = computed(() => {
const lists = new Set() const lists = [] as ShoppingList[]
for (let entry of entries.value) { entries.value.forEach(e => {
if (entry.shoppingLists) { if (e.shoppingLists) {
entry.shoppingLists.forEach(l => lists.add(l)) e.shoppingLists.forEach(l => {
if (lists.findIndex(sl => sl.id == l.id) == -1) {
lists.push(l)
}
})
} }
} })
return Array.from(lists)
return lists
}) })
@@ -138,34 +141,34 @@ const amounts = computed((): ShoppingLineAmount[] => {
for (let i in entries.value) { for (let i in entries.value) {
let e = entries.value[i] 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 unit = -1
if (e.unit !== undefined && e.unit !== null) {
unit = e.unit.id!
}
let uaMerged = false if (e.amount > 0) {
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) { let uaMerged = false
unitAmounts.push({ unitAmounts.forEach(ua => {
key: `${unit}_${e.checked}_${isDelayed(e)}`, if (((ua.unit == null && e.unit == null) || (ua.unit != null && ua.unit.id! == unit)) && ua.checked == e.checked && ua.delayed == isDelayed(e)) {
amount: e.amount, ua.amount += e.amount
unit: e.unit, uaMerged = true
checked: e.checked,
delayed: isDelayed(e)
} as ShoppingLineAmount)
} }
})
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 return unitAmounts
}) })
@@ -186,29 +189,28 @@ const infoRow = computed(() => {
for (let i in entries.value) { for (let i in entries.value) {
let e = entries.value[i] 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 (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) { if (useUserPreferenceStore().deviceSettings.shopping_item_info_created_by && authors.length > 0) {
@@ -266,18 +268,18 @@ function handleSwipe() {
/* 2. Container to wrap the color bars and place them to the far left */ /* 2. Container to wrap the color bars and place them to the far left */
.color-marker-container { .color-marker-container {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 3px; width: 3px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.color-marker-container span { .color-marker-container span {
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<v-tabs v-model="currentTab"> <v-tabs v-model="currentTab">
<v-tab value="shopping"><i class="fas fa-fw" <v-tab value="shopping"><i class="fas fa-fw"
:class="{'fa-circle-notch fa-spin':useShoppingStore().currentlyUpdating, 'fa-shopping-cart ': !useShoppingStore().currentlyUpdating}"></i> <span :class="{'fa-circle-notch fa-spin':useShoppingStore().currentlyUpdating, 'fa-shopping-cart ': !useShoppingStore().currentlyUpdating}"></i> <span
class="d-none d-md-block ms-1">{{ $t('Shopping_list') }} ({{ useShoppingStore().stats.countUnchecked }})</span></v-tab> class="d-none d-md-block ms-1">{{ $t('Shopping_list') }} ({{ useShoppingStore().totalFoods }})</span></v-tab>
<v-tab value="recipes"><i class="fas fa-book fa-fw"></i> <span class="d-none d-md-block ms-1">{{ <v-tab value="recipes"><i class="fas fa-book fa-fw"></i> <span class="d-none d-md-block ms-1">{{
$t('Recipes') $t('Recipes')
}} ({{ useShoppingStore().getAssociatedRecipes().length }})</span></v-tab> }} ({{ useShoppingStore().getAssociatedRecipes().length }})</span></v-tab>
@@ -153,7 +153,7 @@
</v-list> </v-list>
<v-list class="mt-3" density="compact" v-else> <v-list class="mt-3" density="compact" v-else>
<template v-for="category in useShoppingStore().getEntriesByGroup" :key="category.name"> <template v-for="category in useShoppingStore().getEntriesByGroup" :key="category.name">
<template v-if="isShoppingCategoryVisible(category)">
<v-list-subheader v-if="category.name === useShoppingStore().UNDEFINED_CATEGORY"><i>{{ $t('NoCategory') }}</i></v-list-subheader> <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-list-subheader v-else>{{ category.name }}</v-list-subheader>
@@ -163,7 +163,6 @@
<shopping-line-item :shopping-list-food="value"></shopping-line-item> <shopping-line-item :shopping-list-food="value"></shopping-line-item>
</template> </template>
</template>
</template> </template>
</v-list> </v-list>

View File

@@ -25,14 +25,6 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let supermarketCategories = ref([] as SupermarketCategory[]) let supermarketCategories = ref([] as SupermarketCategory[])
let supermarkets = ref([] as Supermarket[]) let supermarkets = ref([] as Supermarket[])
let stats = ref({
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats)
// internal // internal
let currentlyUpdating = ref(false) let currentlyUpdating = ref(false)
let initialized = ref(false) let initialized = ref(false)
@@ -44,26 +36,21 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let undoStack = ref([] as ShoppingOperationHistoryEntry[]) let undoStack = ref([] as ShoppingOperationHistoryEntry[])
let queueTimeoutId = ref(-1) let queueTimeoutId = ref(-1)
let itemCheckSyncQueue = ref([] as IShoppingSyncQueueEntry[]) let itemCheckSyncQueue = ref([] as IShoppingSyncQueueEntry[])
let syncQueueRunning = ref(false)
/** /**
* build a multi-level data structure ready for display from shopping list entries * build a multi-level data structure ready for display from shopping list entries
* group by selected grouping key * group by selected grouping key
*/ */
const getEntriesByGroup = computed(() => { const getEntriesByGroup = computed(() => {
console.log("--> getEntriesByGroup called") console.log('-> getEntriesByGroup called')
stats.value = {
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats
let structure = {} as IShoppingList let structure = {} as IShoppingList
structure.categories = new Map<string, IShoppingListCategory> structure.categories = new Map<string, IShoppingListCategory>
if (useUserPreferenceStore().deviceSettings.shopping_selected_grouping === ShoppingGroupingOptions.CATEGORY && useUserPreferenceStore().deviceSettings.shopping_selected_supermarket != null) { const deviceSettings = useUserPreferenceStore().deviceSettings
useUserPreferenceStore().deviceSettings.shopping_selected_supermarket.categoryToSupermarket.forEach(cTS => {
if (deviceSettings.shopping_selected_grouping === ShoppingGroupingOptions.CATEGORY && deviceSettings.shopping_selected_supermarket != null) {
deviceSettings.shopping_selected_supermarket.categoryToSupermarket.forEach(cTS => {
structure.categories.set(cTS.category.name, {'name': cTS.category.name, 'foods': new Map<number, IShoppingListFood>} as IShoppingListCategory) structure.categories.set(cTS.category.name, {'name': cTS.category.name, 'foods': new Map<number, IShoppingListFood>} as IShoppingListCategory)
}) })
} }
@@ -72,60 +59,50 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
// build structure // build structure
entries.value.forEach(shoppingListEntry => { entries.value.forEach(shoppingListEntry => {
structure = updateEntryInStructure(structure, shoppingListEntry) if (isEntryVisible(shoppingListEntry, deviceSettings)) {
}) structure = updateEntryInStructure(structure, shoppingListEntry)
}
// statistics for UI conditions and display
structure.categories.forEach(category => {
let categoryStats = {
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
countUncheckedDelayed: 0,
} as ShoppingListStats
category.foods.forEach(food => {
let food_checked = true
food.entries.forEach(entry => {
if (entry.checked) {
categoryStats.countChecked++
} else {
if (isDelayed(entry)) {
categoryStats.countUncheckedDelayed++
} else {
categoryStats.countUnchecked++
}
}
})
if (food_checked) {
categoryStats.countCheckedFood++
} else {
categoryStats.countUncheckedFood++
}
})
category.stats = categoryStats
stats.value.countChecked += categoryStats.countChecked
stats.value.countUnchecked += categoryStats.countUnchecked
stats.value.countCheckedFood += categoryStats.countCheckedFood
stats.value.countUncheckedFood += categoryStats.countUncheckedFood
}) })
// ordering // ordering
let undefinedCategoryGroup = structure.categories.get(UNDEFINED_CATEGORY) let undefinedCategoryGroup = structure.categories.get(UNDEFINED_CATEGORY)
if (undefinedCategoryGroup != null) { if (undefinedCategoryGroup != null) {
totalFoods.value += undefinedCategoryGroup.foods.size
orderedStructure.push(undefinedCategoryGroup) orderedStructure.push(undefinedCategoryGroup)
structure.categories.delete(UNDEFINED_CATEGORY) structure.categories.delete(UNDEFINED_CATEGORY)
} }
structure.categories.forEach(category => { structure.categories.forEach(category => {
orderedStructure.push(category) if (category.foods.size > 0) {
orderedStructure.push(category)
}
}) })
return orderedStructure return orderedStructure
}, {
onTrack(e) {
// triggered when count.value is tracked as a dependency
},
onTrigger(e) {
// triggered when count.value is mutated
console.log('TRIGGER', e)
}
})
/**
* get the total number of foods in the shopping list
* since entries are always grouped by food, it makes no sense to display the entry count anywhere
*/
let totalFoods = computed(() => {
let count = 0
if (initialized.value){
getEntriesByGroup.value.forEach(category => {
count += category.foods.size
})
}
return count
}) })
/** /**
@@ -176,7 +153,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
function hasFailedItems() { function hasFailedItems() {
for (let i in itemCheckSyncQueue.value) { for (let i in itemCheckSyncQueue.value) {
if (itemCheckSyncQueue.value[i]['status'] === 'syncing_failed_before' || itemCheckSyncQueue.value[i]['status'] === 'waiting_failed_before') { if (itemCheckSyncQueue.value[i]['status'] === 'syncing_failed_before' || itemCheckSyncQueue.value[i]['status'] === 'waiting_failed_before') {
return true return !syncQueueRunning.value
} }
} }
return false return false
@@ -198,6 +175,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
} else { } else {
// only clear local entries when not given a meal plan to not accidentally filter the shopping list // only clear local entries when not given a meal plan to not accidentally filter the shopping list
entries.value = new Map<number, ShoppingListEntry> entries.value = new Map<number, ShoppingListEntry>
initialized.value = false
} }
recLoadShoppingListEntries(requestParameters) recLoadShoppingListEntries(requestParameters)
@@ -223,19 +201,28 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
function recLoadShoppingListEntries(requestParameters: ApiShoppingListEntryListRequest) { function recLoadShoppingListEntries(requestParameters: ApiShoppingListEntryListRequest) {
let api = new ApiApi() let api = new ApiApi()
return api.apiShoppingListEntryList(requestParameters).then((r) => { return api.apiShoppingListEntryList(requestParameters).then((r) => {
let promises = [] as Promise<any>[]
let newMap = new Map<number, ShoppingListEntry>()
r.results.forEach((e) => { r.results.forEach((e) => {
entries.value.set(e.id!, e) newMap.set(e.id!, e)
}) })
// bulk assign to avoid unnecessary reactivity updates
entries.value = new Map([...entries.value, ...newMap])
if (requestParameters.page == 1 && r.next) { if (requestParameters.page == 1) {
while (Math.ceil(r.count / requestParameters.pageSize) > requestParameters.page) { if (r.next) {
requestParameters.page = requestParameters.page + 1 while (Math.ceil(r.count / requestParameters.pageSize) > requestParameters.page) {
recLoadShoppingListEntries(requestParameters) requestParameters.page = requestParameters.page + 1
promises.push(recLoadShoppingListEntries(requestParameters))
}
} }
} else {
currentlyUpdating.value = false Promise.allSettled(promises).then(() => {
initialized.value = true currentlyUpdating.value = false
initialized.value = true
})
} }
}).catch((err) => { }).catch((err) => {
currentlyUpdating.value = false currentlyUpdating.value = false
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err) useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
@@ -413,15 +400,18 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let api = new ApiApi() let api = new ApiApi()
let promises: Promise<void>[] = [] let promises: Promise<void>[] = []
itemCheckSyncQueue.value.forEach((entry, index) => { let updatedEntries = new Map<number, ShoppingListEntry>()
entry['status'] = ((entry['status'] === 'waiting') ? 'syncing' : 'syncing_failed_before')
itemCheckSyncQueue.value.forEach((entry, index) => {
entry['status'] = ((entry['status'] === 'waiting_failed_before') ? 'syncing_failed_before' : 'syncing')
syncQueueRunning.value = true
let p = api.apiShoppingListEntryBulkCreate({shoppingListEntryBulk: entry}, {}).then((r) => { let p = api.apiShoppingListEntryBulkCreate({shoppingListEntryBulk: entry}, {}).then((r) => {
entry.ids.forEach(id => { entry.ids.forEach(id => {
let e = entries.value.get(id) let e = entries.value.get(id)
e.updatedAt = r.timestamp if (e) {
e.checked = r.checked e.updatedAt = r.timestamp
entries.value.set(id, e) updatedEntries.set(id, e)
}
}) })
itemCheckSyncQueue.value.splice(index, 1) itemCheckSyncQueue.value.splice(index, 1)
}).catch((err) => { }).catch((err) => {
@@ -436,6 +426,8 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
}) })
Promise.allSettled(promises).finally(() => { Promise.allSettled(promises).finally(() => {
entries.value = new Map([...entries.value, ...updatedEntries])
syncQueueRunning.value = false
if (itemCheckSyncQueue.value.length > 0) { if (itemCheckSyncQueue.value.length > 0) {
runSyncQueue(500) runSyncQueue(500)
} }
@@ -593,7 +585,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
hasFailedItems, hasFailedItems,
itemCheckSyncQueue, itemCheckSyncQueue,
undoStack, undoStack,
stats, totalFoods,
refreshFromAPI, refreshFromAPI,
autoSync, autoSync,
createObject, createObject,