lots of work on shopping store

This commit is contained in:
vabene1111
2024-10-16 20:58:56 +02:00
parent e8522a4a6d
commit 7fd402aade
5 changed files with 229 additions and 226 deletions

View File

@@ -12,7 +12,6 @@
"@types/luxon": "^3.4.2",
"@vueform/multiselect": "^2.6.8",
"@vueuse/core": "^10.11.0",
"lodash": "^4.17.21",
"luxon": "^3.4.4",
"mavon-editor": "^3.0.1",
"pinia": "^2.1.7",

View File

@@ -15,6 +15,7 @@
<template v-for="category in useShoppingStore().getEntriesByGroup">
<v-list-subheader>{{ category.name }}</v-list-subheader>
{{category.stats}}
<v-divider></v-divider>
<template v-for="item in category.foods">
@@ -50,8 +51,6 @@ import {useShoppingStore} from "@/stores/ShoppingStore";
const currentTab = ref("shopping")
const shoppingStore = useShoppingStore()
useShoppingStore().refreshFromAPI()
</script>

View File

@@ -1,19 +1,22 @@
import {acceptHMRUpdate, defineStore} from "pinia"
import {ApiApi, MealPlan, ShoppingListEntry, Supermarket, SupermarketCategory} from "@/openapi";
import {ApiApi, ShoppingListEntry, Supermarket, SupermarketCategory} from "@/openapi";
import {computed, ref} from "vue";
import {DateTime} from "luxon";
import _ from 'lodash';
import {IShoppingList, IShoppingListCategory, IShoppingListFood} from "@/types/Shopping";
import {
IShoppingExportEntry,
IShoppingList,
IShoppingListCategory,
IShoppingListFood,
IShoppingSyncQueueEntry,
ShoppingGroupingOptions, ShoppingListStats,
ShoppingOperationHistoryEntry,
ShoppingOperationHistoryType
} from "@/types/Shopping";
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
const _STORE_ID = "shopping_store"
const UNDEFINED_CATEGORY = 'shopping_undefined_category'
enum GroupingOptions {
GROUP_CATEGORY = 'food.supermarketCategory.name',
GROUP_CREATED_BY = 'createdBy.displayName',
GROUP_RECIPE = 'recipeMealplan.recipeName',
}
export const useShoppingStore = defineStore(_STORE_ID, () => {
let entries = ref(new Map<number, ShoppingListEntry>)
let supermarketCategories = ref([] as SupermarketCategory[])
@@ -25,14 +28,20 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let total_unchecked_food = ref(0)
let total_checked_food = ref(0)
let stats = ref({
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
} as ShoppingListStats)
// internal
let currentlyUpdating = ref(false)
let lastAutosync = ref(0)
let autosync_has_focus = ref(true)
let autosync_timeout_id = ref(null) // TODO number?
let undo_stack = ref([] as MealPlan[]) //TODO custom type for undo stack
let queue_timeout_id = ref(null) // TODO number?
let item_check_sync_queue = ref(null) // TODO custom type for check queue?
let autosyncHasFocus = ref(true)
let undoStack = ref([] as ShoppingOperationHistoryEntry[])
let queueTimeoutId = ref(-1)
let itemCheckSyncQueue = ref([] as IShoppingSyncQueueEntry[])
/**
@@ -42,74 +51,58 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
*/
const getEntriesByGroup = computed(() => {
let structure = {} as IShoppingList
structure.categories = new Map<number, IShoppingListCategory>
structure.categories = new Map<string, IShoppingListCategory>
let ordered_structure = [] as IShoppingListCategory[]
let orderedStructure = [] as IShoppingListCategory[]
// build structure
for (let i in entries.value.keys()) {
}
entries.value.forEach(shoppingListEntry => {
structure = updateEntryInStructure(structure, shoppingListEntry, GroupingOptions.GROUP_CATEGORY) // TODO take category from device settings
structure = updateEntryInStructure(structure, shoppingListEntry, useUserPreferenceStore().deviceSettings.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
structure.categories.forEach(category => {
let categoryStats = {
countChecked: 0,
countUnchecked: 0,
countCheckedFood: 0,
countUncheckedFood: 0,
} as ShoppingListStats
// for (let i: number in structure.categories.keys()) {
// let count_unchecked = 0
// let count_checked = 0
// let count_unchecked_food = 0
// let count_checked_food = 0
//
// for (let fi: number in structure.categories.get(i).foods.keys()) {
// let food_checked = true
// for (let ei in structure.categories.get(i).foods.get(fi).entries.key()) {
// 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
category.foods.forEach(food => {
let food_checked = true
food.entries.forEach(entry => {
if (entry.checked) {
categoryStats.countChecked++
} 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
if (structure.categories.has(UNDEFINED_CATEGORY)) {
ordered_structure.push(structure.categories.get(UNDEFINED_CATEGORY))
orderedStructure.push(structure.categories.get(UNDEFINED_CATEGORY))
structure.categories.delete(UNDEFINED_CATEGORY)
}
structure.categories.forEach(category => {
ordered_structure.push(category)
orderedStructure.push(category)
})
// TODO implement ordering
@@ -131,55 +124,44 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
// }
// }
return ordered_structure
return orderedStructure
})
/**
* flattened list of entries used for exporters
* kinda uncool but works for now
* @return {*[]}
* @return IShoppingExportEntry[]
*/
function getFlatEntries() {
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
}
let items: IShoppingExportEntry[] = []
/**
* list of options available for grouping entry display
* @return array of grouping options
*/
function groupingOptions() {
return [
{'id': GroupingOptions.GROUP_CATEGORY, 'translationKey': 'Category'} as IGroupingOption,
{'id': GroupingOptions.GROUP_CREATED_BY, 'translationKey': 'created_by'} as IGroupingOption,
{'id': GroupingOptions.GROUP_RECIPE, 'translationKey': 'Recipe'} as IGroupingOption,
]
getEntriesByGroup.value.forEach(shoppingListEntry => {
shoppingListEntry.foods.forEach(food => {
food.entries.forEach(entry => {
items.push({
amount: entry.amount,
unit: entry.unit?.name ?? '',
food: entry.food?.name ?? '',
})
})
})
})
return items
}
/**
* checks if failed items are contained in the sync queue
*/
function hasFailedItems() {
for (let i in item_check_sync_queue.value) {
if (item_check_sync_queue.value[i]['status'] === 'syncing_failed_before' || item_check_sync_queue.value[i]['status'] === 'waiting_failed_before') {
for (let i in itemCheckSyncQueue.value) {
if (itemCheckSyncQueue.value[i]['status'] === 'syncing_failed_before' || itemCheckSyncQueue.value[i]['status'] === 'waiting_failed_before') {
return true
}
}
return false
}
//TODO fix/verify for typescript
/**
* Retrieves all shopping related data (shopping list entries, supermarkets, supermarket categories and shopping list recipes) from API
*/
@@ -191,112 +173,109 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let api = new ApiApi()
api.apiShoppingListEntryList().then((r) => {
entries.value = new Map<number, ShoppingListEntry>
r.forEach((e) => {
entries.value.set(e.id, e)
// TODO load all pages
r.results.forEach((e) => {
entries.value.set(e.id!, e)
})
currentlyUpdating.value = false
}).catch((err) => {
currentlyUpdating.value = false
// TODO add message to log
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
api.apiSupermarketList().then(r => {
supermarketCategories.value = r
supermarketCategories.value = r.results
}).catch((err) => {
// TODO add message to log
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
api.apiSupermarketList().then(r => {
supermarkets.value = r
supermarkets.value = r.results
}).catch((err) => {
// TODO add message to log
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
}
//TODO fix/verify for typescript
/**
* 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
*/
function autosync() {
if (!this.currently_updating && this.autosync_has_focus) {
if (!currentlyUpdating.value && autosyncHasFocus.value) {
console.log('running autosync')
this.currently_updating = true
currentlyUpdating.value = true
let previous_autosync = this.lastAutosync
this.lastAutosync = new Date().getTime();
let previous_autosync = lastAutosync.value
lastAutosync.value = new Date().getTime();
const api = new ApiApi()
// TODO query parameters
api.listShoppingListEntrys({lastAutosync: previous_autosync}).then((r) => {
r.forEach((e) => {
// TODO implement parameters on backend Oepnapi
api.apiShoppingListEntryList({lastAutosync: previous_autosync}).then((r) => {
r.results.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)) {
if (!entries.value.has(e.id!) || entries.value.get(e.id!).updatedAt < e.updatedAt) {
console.log('auto sync updating entry ', e)
this.entries[e.id] = e
entries.value.set(e.id!, e)
}
})
this.currently_updating = false
}).catch((err) => {
currentlyUpdating.value = false
}).catch((err: any) => {
console.warn('auto sync failed')
this.currently_updating = false
currentlyUpdating.value = false
})
}
}
//TODO fix/verify for typescript
/**
* 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
* @return {Promise<ShoppingListEntry>} promise of creation call to subscribe to
*/
function createObject(object) {
function createObject(object: ShoppingListEntry) {
const api = new ApiApi()
return api.createShoppingListEntry(object).then((r) => {
this.entries[r.id] = r
return api.apiShoppingListEntryCreate({shoppingListEntry: object}).then((r) => {
entries.value.set(r.id!, r)
}).catch((err) => {
// StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
})
}
//TODO fix/verify for typescript
/**
* 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
* @return {Promise<ShoppingListEntry>} promise of updating call to subscribe to
*/
function updateObject(object) {
function updateObject(object: ShoppingListEntry) {
const api = new ApiApi()
// 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())
object['update_at'] = DateTime.toLocaleString() // TODO check formats
// object.updatedAt = DateTime.toLocaleString()
// TODO setting timestamp on the client does not make sense because client and server clock might be out of sync and field will be overridden by server anyway
return api.updateShoppingListEntry(object.id, object).then((r) => {
this.entries[r.id] = r
return api.apiShoppingListEntryUpdate({id: object.id!, shoppingListEntry: object}).then((r) => {
entries.value.set(r.id!, r)
}).catch((err) => {
// StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
}
//TODO fix/verify for typescript
/**
* 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
* @return {Promise<ShoppingListEntry>} promise of delete call to subscribe to
*/
function deleteObject(object) {
function deleteObject(object: ShoppingListEntry) {
const api = new ApiApi()
return api.destroyShoppingListEntry({id: object.id}).then((r) => {
delete this.entries[object.id]
return api.apiShoppingListEntryDestroy({id: object.id!}).then((r) => {
entries.value.delete(object.id!)
}).catch((err) => {
// StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
})
}
@@ -325,7 +304,6 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
}
// convenience methods
//TODO fix/verify for typescript
/**
* function to set entry to its proper place in the data structure to perform grouping
* @param {{}} structure datastructure
@@ -333,54 +311,56 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
* @param {*} group group to place entry into (must be of ShoppingListStore.GROUP_XXX/dot notation of entry property)
* @returns {{}} datastructure including entry
*/
function updateEntryInStructure(structure: IShoppingList, entry: ShoppingListEntry, group: GroupingOptions) {
let grouping_key = _.get(entry, group, UNDEFINED_CATEGORY)
function updateEntryInStructure(structure: IShoppingList, entry: ShoppingListEntry, group: ShoppingGroupingOptions) {
let groupingKey = UNDEFINED_CATEGORY
if (grouping_key === undefined || grouping_key === null) {
grouping_key = UNDEFINED_CATEGORY
if (group == ShoppingGroupingOptions.CATEGORY && entry.food != null && entry.food.supermarketCategory != null) {
groupingKey = entry.food?.supermarketCategory?.name
} else if (group == ShoppingGroupingOptions.CREATED_BY) {
groupingKey = entry.createdBy.displayName
} else if (group == ShoppingGroupingOptions.RECIPE && entry.recipeMealplan != null) {
groupingKey = entry.recipeMealplan.recipeName
}
if (!structure.categories.has(grouping_key)) {
structure.categories.set(grouping_key, {'name': grouping_key, 'foods': new Map<number, IShoppingListFood>} as IShoppingListCategory)
if (!structure.categories.has(groupingKey)) {
structure.categories.set(groupingKey, {'name': groupingKey, 'foods': new Map<number, IShoppingListFood>} as IShoppingListCategory)
}
if (!structure.categories.get(grouping_key).foods.has(entry.food.id)) {
structure.categories.get(grouping_key).foods.set(entry.food.id, {
if (!structure.categories.get(groupingKey).foods.has(entry.food.id)) {
structure.categories.get(groupingKey).foods.set(entry.food.id, {
food: entry.food,
entries: new Map<number, ShoppingListEntry>
} as IShoppingListFood)
}
structure.categories.get(grouping_key).foods.get(entry.food.id).entries.set(entry.id, entry)
structure.categories.get(groupingKey).foods.get(entry.food.id).entries.set(entry.id, entry)
return structure
}
//TODO fix/verify for typescript
/**
* 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
*/
function setEntriesCheckedState(entries, checked, undo) {
function setEntriesCheckedState(entries: ShoppingListEntry[], checked: boolean, undo: boolean) {
if (undo) {
this.registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
registerChange((checked ? 'CHECKED' : 'UNCHECKED'), entries)
}
let entry_id_list = []
for (let i in entries) {
this.entries[i]['checked'] = checked
this.entries[i]['updated_at'] = DateTime.now().toISOTime() // TODO was moment.format() (see above)
entry_id_list.push(i)
}
let entryIdList: number[] = []
entries.forEach(entry => {
entry.checked = checked
// TODO used to set updatedAt but does not make sense on client, rethink solution (as above)
entryIdList.push(entry.id!)
})
this.item_check_sync_queue[Math.random()] = {
'ids': entry_id_list,
'checked': checked,
'status': 'waiting'
}
this.runSyncQueue(5)
itemCheckSyncQueue.value.push({
ids: entryIdList,
checked: checked,
status: 'waiting',
} as IShoppingSyncQueueEntry)
runSyncQueue(5)
}
//TODO fix/verify for typescript
/**
* 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
@@ -392,99 +372,95 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
let api = new ApiApi()
let promises = []
for (let i in this.item_check_sync_queue) {
let entry = this.item_check_sync_queue[i]
for (let i in itemCheckSyncQueue.value) {
let entry = itemCheckSyncQueue.value[i]
entry['status'] = ((entry['status'] === 'waiting') ? 'syncing' : 'syncing_failed_before')
this.item_check_sync_queue[i] = entry
itemCheckSyncQueue.value[i] = entry
// TODO request params
let p = api.bulkShoppingListEntry(entry, {timeout: 15000}).then((r) => {
delete this.item_check_sync_queue[i]
// TODO set timeout for request (previously was 15000ms) or check that default timeout is similar
let p = api.apiShoppingListEntryBulkCreate({shoppingListEntryBulk: entry}, {}).then((r) => {
delete itemCheckSyncQueue.value[i]
}).catch((err) => {
if (err.code === "ERR_NETWORK" || err.code === "ECONNABORTED") {
entry['status'] = 'waiting_failed_before'
this.item_check_sync_queue[i] = entry
itemCheckSyncQueue.value[i] = entry
} else {
delete this.item_check_sync_queue[i]
delete itemCheckSyncQueue.value[i]
console.error('Failed API call for entry ', entry)
// StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}
})
promises.push(p)
}
Promise.allSettled(promises).finally(r => {
this.runSyncQueue(500)
Promise.allSettled(promises).finally(() => {
runSyncQueue(500)
})
} else {
this.runSyncQueue(5000)
runSyncQueue(5000)
}
}
//TODO fix/verify for typescript
/**
* 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
*/
function runSyncQueue(timeout) {
clearTimeout(this.queue_timeout_id)
function runSyncQueue(timeout: number) {
clearTimeout(queueTimeoutId.value)
this.queue_timeout_id = setTimeout(() => {
this._replaySyncQueue()
queueTimeoutId.value = window.setTimeout(() => {
_replaySyncQueue()
}, timeout)
}
//TODO fix/verify for typescript
/**
* 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
*/
function delayEntries(entries, delay, undo) {
let delay_hours = useUserPreferenceStore().user_settings.default_delay
let delay_date = new Date(Date.now() + delay_hours * (60 * 60 * 1000))
function delayEntries(entries: ShoppingListEntry[], delay: boolean, undo: boolean) {
let delay_hours = useUserPreferenceStore().userSettings.defaultDelay!
let delayDate = new Date(Date.now() + delay_hours * (60 * 60 * 1000))
if (undo) {
this.registerChange((delay ? 'DELAY' : 'UNDELAY'), entries)
registerChange((delay ? 'DELAY' : 'UNDELAY'), entries)
}
for (let i in entries) {
this.entries[i].delay_until = (delay ? delay_date : null)
this.updateObject(this.entries[i])
entries[i].delayUntil = (delay ? delayDate : null)
updateObject(entries[i])
}
}
//TODO fix/verify for typescript
/**
* delete list of entries
* @param {{}} entries set of entries
*/
function deleteEntries(entries) {
for (let i in entries) {
this.deleteObject(this.entries[i])
}
}
//TODO fix/verify for typescript
function deleteShoppingListRecipe(shopping_list_recipe_id) {
const api = new ApiApi()
for (let i in this.entries) {
if (this.entries[i].list_recipe === shopping_list_recipe_id) {
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)
function deleteEntries(entries: ShoppingListEntry[]) {
entries.forEach((entry) => {
deleteObject(entry)
})
}
//TODO fix/verify for typescript
function deleteShoppingListRecipe(shopping_list_recipe_id: number) {
const api = new ApiApi()
entries.value.forEach(entry => {
if (entry.listRecipe == shopping_list_recipe_id) {
entries.value.delete(entry.id!)
}
})
api.apiShoppingListRecipeDestroy({id: shopping_list_recipe_id}).then((x) => {
// no need to update anything, entries were already removed
}).catch((err) => {
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
})
}
/**
* register the change to a set of entries to allow undoing it
* throws an Error if the operation type is not known
@@ -492,31 +468,27 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
* CHECKED->uncheck entry, UNCHECKED->check entry, DELAY->remove delay)
* @param {{}} entries set of entries
*/
function 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})
function registerChange(type: ShoppingOperationHistoryType, entries: ShoppingListEntry[]) {
undoStack.value.push({'type': type, 'entries': entries} as ShoppingOperationHistoryEntry)
}
//TODO fix/verify for typescript
/**
* takes the last item from the undo stack and reverts it
*/
function undoChange() {
let last_item = this.undo_stack.pop()
let last_item = undoStack.value.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)
setEntriesCheckedState(entries, (type === 'UNCHECKED'), false)
} else if (type === 'DELAY' || type === 'UNDELAY') {
this.delayEntries(entries, (type === 'UNDELAY'), false)
delayEntries(entries, (type === 'UNDELAY'), false)
} else if (type === 'CREATED') {
for (let i in entries) {
let e = entries[i]
this.deleteObject(e)
deleteObject(e)
}
}
} else {
@@ -525,9 +497,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
}
}
return {entries, supermarkets, supermarketCategories, getEntriesByGroup, getFlatEntries, groupingOptions, hasFailedItems, refreshFromAPI}
return {entries, supermarkets, supermarketCategories, getEntriesByGroup, getFlatEntries, hasFailedItems, refreshFromAPI}
})
// enable hot reload for store

View File

@@ -2,6 +2,7 @@ import {acceptHMRUpdate, defineStore} from 'pinia'
import {useStorage} from "@vueuse/core";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
import {ApiApi, ServerSettings, Space, UserPreference} from "@/openapi";
import {ShoppingGroupingOptions} from "@/types/Shopping";
const DEVICE_SETTINGS_KEY = 'TANDOOR_DEVICE_SETTINGS'
const USER_PREFERENCE_KEY = 'TANDOOR_USER_PREFERENCE'
@@ -12,7 +13,7 @@ class DeviceSettings {
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_grouping = ShoppingGroupingOptions.CATEGORY
shopping_selected_supermarket = null
shopping_item_info_created_by = false
shopping_item_info_mealplan = false

View File

@@ -1,4 +1,12 @@
import {Food, ShoppingListEntry, SupermarketCategory} from "@/openapi";
import {b} from "vite/dist/node/types.d-aGj9QkWt";
import {ref} from "vue";
export enum ShoppingGroupingOptions {
CATEGORY = 'CATEGORY',
CREATED_BY = 'CREATED_BY',
RECIPE = 'RECIPE',
}
export interface IShoppingList {
categories: Map<string, IShoppingListCategory>
@@ -6,7 +14,8 @@ export interface IShoppingList {
export interface IShoppingListCategory {
name: string,
foods: Map<number, IShoppingListFood>
foods: Map<number, IShoppingListFood>,
stats: ShoppingListStats,
}
export interface IShoppingListFood {
@@ -17,4 +26,29 @@ export interface IShoppingListFood {
export interface IGroupingOption {
id: string,
translationKey: string
}
export interface IShoppingExportEntry {
amount: number,
unit: string,
food: string,
}
export interface IShoppingSyncQueueEntry {
ids: number[],
checked: boolean,
status: 'waiting' | 'syncing' | 'syncing_failed_before' | 'waiting_failed_before',
}
export type ShoppingListStats = {
countChecked: number,
countUnchecked: number,
countCheckedFood: number,
countUncheckedFood: number,
}
export type ShoppingOperationHistoryType = 'CREATED' | 'CHECKED' | 'UNCHECKED' | 'DELAY' | 'UNDELAY'
export type ShoppingOperationHistoryEntry = {
type: ShoppingOperationHistoryType,
entries: ShoppingListEntry[]
}