diff --git a/vue3/src/pages/ShoppingListPage.vue b/vue3/src/pages/ShoppingListPage.vue
index 2212b9458..6eecae904 100644
--- a/vue3/src/pages/ShoppingListPage.vue
+++ b/vue3/src/pages/ShoppingListPage.vue
@@ -1,12 +1,37 @@
- Shopping List
+
+
+
+ Super
+ Super
+
+
diff --git a/vue3/src/stores/ShoppingListStore.js b/vue3/src/stores/ShoppingListStore.js
new file mode 100644
index 000000000..34af42fbf
--- /dev/null
+++ b/vue3/src/stores/ShoppingListStore.js
@@ -0,0 +1,490 @@
+import {defineStore} from "pinia"
+import Vue from "vue"
+import _ from 'lodash';
+import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
+import {ApiApi} from "@/openapi/index.js";
+import {DateTime} from "luxon";
+
+
+const _STORE_ID = "shopping_list_store"
+/*
+ * test store to play around with pinia and see if it can work for my use cases
+ * don't trust that all shopping list entries are in store as there is no cache validation logic, its just a shared data holder
+ * */
+export const useShoppingListStore = defineStore(_STORE_ID, {
+ state: () => ({
+ // shopping data
+ entries: {},
+ supermarket_categories: [],
+ supermarkets: [],
+
+ total_unchecked: 0,
+ total_checked: 0,
+ total_unchecked_food: 0,
+ total_checked_food: 0,
+
+ // internal
+ currently_updating: false,
+ last_autosync: null,
+ autosync_has_focus: true,
+ autosync_timeout_id: null,
+ undo_stack: [],
+
+ queue_timeout_id: undefined,
+ item_check_sync_queue: {},
+
+ // constants
+ GROUP_CATEGORY: 'food.supermarket_category.name',
+ GROUP_CREATED_BY: 'created_by.display_name',
+ GROUP_RECIPE: 'recipe_mealplan.recipe_name',
+
+ UNDEFINED_CATEGORY: 'shopping_undefined_category'
+ }),
+ getters: {
+ /**
+ * build a multi-level data structure ready for display from shopping list entries
+ * group by selected grouping key
+ * @return {{}}
+ */
+ get_entries_by_group: function () {
+ let structure = {}
+ let ordered_structure = []
+
+ // build structure
+ for (let i in this.entries) {
+ structure = this.updateEntryInStructure(structure, this.entries[i], useUserPreferenceStore().device_settings.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
+ for (let i in structure) {
+ let count_unchecked = 0
+ let count_checked = 0
+ let count_unchecked_food = 0
+ let count_checked_food = 0
+
+ for (let fi in structure[i]['foods']) {
+ let food_checked = true
+ for (let ei in structure[i]['foods'][fi]['entries']) {
+ 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
+
+ // ordering
+ if (this.UNDEFINED_CATEGORY in structure) {
+ ordered_structure.push(structure[this.UNDEFINED_CATEGORY])
+ Vue.delete(structure, this.UNDEFINED_CATEGORY)
+ }
+
+ if (useUserPreferenceStore().device_settings.shopping_selected_grouping === this.GROUP_CATEGORY && useUserPreferenceStore().device_settings.shopping_selected_supermarket !== null) {
+ for (let c of useUserPreferenceStore().device_settings.shopping_selected_supermarket.category_to_supermarket) {
+ if (c.category.name in structure) {
+ ordered_structure.push(structure[c.category.name])
+ Vue.delete(structure, c.category.name)
+ }
+ }
+ if (!useUserPreferenceStore().device_settings.shopping_show_selected_supermarket_only) {
+ for (let i in structure) {
+ ordered_structure.push(structure[i])
+ }
+ }
+ } else {
+ for (let i in structure) {
+ ordered_structure.push(structure[i])
+ }
+ }
+
+ return ordered_structure
+ },
+ /**
+ * flattened list of entries used for exporters
+ * kinda uncool but works for now
+ * @return {*[]}
+ */
+ get_flat_entries: function () {
+ 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
+ },
+ /**
+ * list of options available for grouping entry display
+ * @return {[{id: *, translatable_label: string},{id: *, translatable_label: string},{id: *, translatable_label: string}]}
+ */
+ grouping_options: function () {
+ return [
+ {'id': this.GROUP_CATEGORY, 'translatable_label': 'Category'},
+ {'id': this.GROUP_CREATED_BY, 'translatable_label': 'created_by'},
+ {'id': this.GROUP_RECIPE, 'translatable_label': 'Recipe'}
+ ]
+ },
+ /**
+ * checks if failed items are contained in the sync queue
+ */
+ has_failed_items: function () {
+ for (let i in this.item_check_sync_queue) {
+ if (this.item_check_sync_queue[i]['status'] === 'syncing_failed_before' || this.item_check_sync_queue[i]['status'] === 'waiting_failed_before') {
+ return true
+ }
+ }
+ return false
+ }
+ },
+ actions: {
+ /**
+ * Retrieves all shopping related data (shopping list entries, supermarkets, supermarket categories and shopping list recipes) from API
+ */
+ refreshFromAPI() {
+ if (!this.currently_updating) {
+ this.currently_updating = true
+ this.last_autosync = new Date().getTime();
+
+ let api = new ApiApi()
+ api.listShoppingListEntrys().then((r) => {
+ this.entries = {}
+
+ r.forEach((e) => {
+ this.entries[e.id] = e
+ })
+ this.currently_updating = false
+ }).catch((err) => {
+ this.currently_updating = false
+ //StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
+ })
+
+ api.listSupermarketCategorys().then(r => {
+ this.supermarket_categories = r
+ }).catch((err) => {
+ // StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
+ })
+
+ api.listSupermarkets().then(r => {
+ this.supermarkets = r
+ }).catch((err) => {
+ // StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
+ })
+ }
+ },
+ /**
+ * 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
+ */
+ autosync() {
+ if (!this.currently_updating && this.autosync_has_focus) {
+ console.log('running autosync')
+
+ this.currently_updating = true
+
+ let previous_autosync = this.last_autosync
+ this.last_autosync = new Date().getTime();
+
+ const api = new ApiApi()
+ // TODO query parameters
+ api.listShoppingListEntrys({last_autosync: previous_autosync}).then((r) => {
+ r.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)) {
+ console.log('auto sync updating entry ', e)
+ this.entries[e.id] = e
+ }
+ })
+ this.currently_updating = false
+ }).catch((err) => {
+ console.warn('auto sync failed')
+ this.currently_updating = false
+ })
+ }
+ },
+ /**
+ * Create a new shopping list entry
+ * adds new entry to store
+ * @param object entry object to create
+ * @return {Promise} promise of creation call to subscribe to
+ */
+ createObject(object) {
+ const api = new ApiApi()
+ return api.createShoppingListEntry(object).then((r) => {
+ this.entries[r.id] = r
+ }).catch((err) => {
+ // StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
+ })
+ },
+ /**
+ * 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} promise of updating call to subscribe to
+ */
+ updateObject(object) {
+ 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
+
+ return api.updateShoppingListEntry(object.id, object).then((r) => {
+ this.entries[r.id] = r
+ }).catch((err) => {
+ // StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
+ })
+ },
+ /**
+ * delete shopping list entry object from DB and store
+ * @param object entry object to delete
+ * @return {Promise} promise of delete call to subscribe to
+ */
+ deleteObject(object) {
+ const api = new ApiApi()
+ return api.destroyShoppingListEntry({id: object.id}).then((r) => {
+ delete this.entries[object.id]
+ }).catch((err) => {
+ // StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
+ })
+ },
+ /**
+ * returns a distinct list of recipes associated with unchecked shopping list entries
+ */
+ getAssociatedRecipes: function () {
+ let recipes = {} // TODO this needs a type
+
+ for (let i in this.entries) {
+ let e = this.entries[i]
+ if (e.recipe_mealplan !== null) {
+ recipes[e.recipe_mealplan.recipe] = {
+ 'shopping_list_recipe_id': e.list_recipe,
+ 'recipe_id': e.recipe_mealplan.recipe,
+ 'recipe_name': e.recipe_mealplan.recipe_name,
+ 'servings': e.recipe_mealplan.servings,
+ 'mealplan_from_date': e.recipe_mealplan.mealplan_from_date,
+ 'mealplan_type': e.recipe_mealplan.mealplan_type,
+ }
+ }
+ }
+
+ return recipes
+ },
+ // convenience methods
+ /**
+ * function to set entry to its proper place in the data structure to perform grouping
+ * @param {{}} structure datastructure
+ * @param {*} entry entry to place
+ * @param {*} group group to place entry into (must be of ShoppingListStore.GROUP_XXX/dot notation of entry property)
+ * @returns {{}} datastructure including entry
+ */
+ updateEntryInStructure(structure, entry, group) {
+ let grouping_key = _.get(entry, group, this.UNDEFINED_CATEGORY)
+
+ if (grouping_key === undefined || grouping_key === null) {
+ grouping_key = this.UNDEFINED_CATEGORY
+ }
+
+ if (!(grouping_key in structure)) {
+ structure[grouping_key] = {'name': grouping_key, 'foods': {}}
+ }
+ if (!(entry.food.id in structure[grouping_key]['foods'])) {
+ structure[grouping_key]['foods'][entry.food.id] = {
+ 'id': entry.food.id,
+ 'name': entry.food.name,
+ 'entries': {}
+ }
+ }
+ structure[grouping_key]['foods'][entry.food.id]['entries'][entry.id] = entry
+ return structure
+ },
+ /**
+ * 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
+ */
+ setEntriesCheckedState(entries, checked, undo) {
+ if (undo) {
+ this.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)
+ }
+
+ this.item_check_sync_queue[Math.random()] = {
+ 'ids': entry_id_list,
+ 'checked': checked,
+ 'status': 'waiting'
+ }
+ this.runSyncQueue(5)
+ },
+ /**
+ * 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
+ * Do NOT call this method directly, always call using runSyncQueue method to prevent simultaneous runs
+ * @private
+ */
+ _replaySyncQueue() {
+ if (navigator.onLine || document.location.href.includes('localhost')) {
+ let api = new ApiApi()
+ let promises = []
+
+ for (let i in this.item_check_sync_queue) {
+ let entry = this.item_check_sync_queue[i]
+ entry['status'] = ((entry['status'] === 'waiting') ? 'syncing' : 'syncing_failed_before')
+ this.item_check_sync_queue[i] = entry
+
+ // TODO request params
+ let p = api.bulkShoppingListEntry(entry, {timeout: 15000}).then((r) => {
+ delete this.item_check_sync_queue[i]
+ }).catch((err) => {
+ if (err.code === "ERR_NETWORK" || err.code === "ECONNABORTED") {
+ entry['status'] = 'waiting_failed_before'
+ this.item_check_sync_queue[i] = entry
+ } else {
+ delete this.item_check_sync_queue[i]
+ console.error('Failed API call for entry ', entry)
+ // StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
+ }
+ })
+ promises.push(p)
+ }
+
+ Promise.allSettled(promises).finally(r => {
+ this.runSyncQueue(500)
+ })
+ } else {
+ this.runSyncQueue(5000)
+ }
+ },
+ /**
+ * 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
+ */
+ runSyncQueue(timeout) {
+ clearTimeout(this.queue_timeout_id)
+
+ this.queue_timeout_id = setTimeout(() => {
+ this._replaySyncQueue()
+ }, timeout)
+ },
+ /**
+ * 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
+ */
+ delayEntries(entries, delay, undo) {
+ let delay_hours = useUserPreferenceStore().user_settings.default_delay
+ let delay_date = new Date(Date.now() + delay_hours * (60 * 60 * 1000))
+
+ if (undo) {
+ this.registerChange((delay ? 'DELAY' : 'UNDELAY'), entries)
+ }
+
+ for (let i in entries) {
+ this.entries[i].delay_until = (delay ? delay_date : null)
+ this.updateObject(this.entries[i])
+ }
+ },
+ /**
+ * delete list of entries
+ * @param {{}} entries set of entries
+ */
+ deleteEntries(entries) {
+ for (let i in entries) {
+ this.deleteObject(this.entries[i])
+ }
+ },
+ 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)
+ })
+ },
+ /**
+ * register the change to a set of entries to allow undoing it
+ * throws an Error if the operation type is not known
+ * @param type the type of change to register. This determines what undoing the change does. (CREATE->delete object,
+ * CHECKED->uncheck entry, UNCHECKED->check entry, DELAY->remove delay)
+ * @param {{}} entries set of entries
+ */
+ 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})
+ },
+ /**
+ * takes the last item from the undo stack and reverts it
+ */
+ undoChange() {
+ let last_item = this.undo_stack.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)
+ } else if (type === 'DELAY' || type === 'UNDELAY') {
+ this.delayEntries(entries, (type === 'UNDELAY'), false)
+ } else if (type === 'CREATED') {
+ for (let i in entries) {
+ let e = entries[i]
+ this.deleteObject(e)
+ }
+ }
+ } else {
+ // can use localization in store
+ //StandardToasts.makeStandardToast(this, this.$t('NoMoreUndo'))
+ }
+ }
+ },
+})
diff --git a/vue3/src/stores/UserPreferenceStore.js b/vue3/src/stores/UserPreferenceStore.js
new file mode 100644
index 000000000..b457cfa57
--- /dev/null
+++ b/vue3/src/stores/UserPreferenceStore.js
@@ -0,0 +1,205 @@
+import {defineStore} from 'pinia'
+
+
+import {ApiApiFactory, UserPreference} from "@/utils/openapi/api";
+import Vue from "vue";
+import {StandardToasts} from "@/utils/utils";
+
+const _STALE_TIME_IN_MS = 1000 * 30
+const _STORE_ID = 'user_preference_store'
+
+const _LS_DEVICE_SETTINGS = 'TANDOOR_LOCAL_SETTINGS'
+const _LS_USER_SETTINGS = 'TANDOOR_USER_SETTINGS'
+const _USER_ID = localStorage.getItem('USER_ID')
+
+export const useUserPreferenceStore = defineStore(_STORE_ID, {
+ state: () => ({
+ data: null,
+ updated_at: null,
+ currently_updating: false,
+
+ user_settings_loaded_at: new Date(0),
+ user_settings: {
+ image: null,
+ theme: "TANDOOR",
+ nav_bg_color: "#ddbf86",
+ nav_text_color: "DARK",
+ nav_show_logo: true,
+ default_unit: "g",
+ default_page: "SEARCH",
+ use_fractions: false,
+ use_kj: false,
+ plan_share: [],
+ nav_sticky: true,
+ ingredient_decimals: 2,
+ comments: true,
+ shopping_auto_sync: 5,
+ mealplan_autoadd_shopping: false,
+ food_inherit_default: [],
+ default_delay: "4.0000",
+ mealplan_autoinclude_related: true,
+ mealplan_autoexclude_onhand: true,
+ shopping_share: [],
+ shopping_recent_days: 7,
+ csv_delim: ",",
+ csv_prefix: "",
+ filter_to_supermarket: false,
+ shopping_add_onhand: false,
+ left_handed: false,
+ show_step_ingredients: true,
+ food_children_exist: false,
+ locally_updated_at: new Date(0),
+ },
+
+ device_settings_initialized: false,
+ device_settings_loaded_at: new Date(0),
+ device_settings: {
+ // shopping
+ 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_supermarket: null,
+ shopping_item_info_created_by: false,
+ shopping_item_info_mealplan: false,
+ shopping_item_info_recipe: true,
+ },
+ }),
+ getters: {},
+ actions: {
+ // Device settings (on device settings stored in local storage)
+ /**
+ * Load device settings from local storage and update state device_settings
+ */
+ loadDeviceSettings() {
+ let s = localStorage.getItem(_LS_DEVICE_SETTINGS)
+ if (s !== null) {
+ let settings = JSON.parse(s)
+ for (s in settings) {
+ Vue.set(this.device_settings, s, settings[s])
+ }
+ }
+ this.device_settings_initialized = true
+ },
+ /**
+ * persist changes to device settings into local storage
+ */
+ updateDeviceSettings: function () {
+ localStorage.setItem(_LS_DEVICE_SETTINGS, JSON.stringify(this.device_settings))
+ },
+ // ---------------- new methods for user settings
+ loadUserSettings: function (allow_cached_results) {
+ let s = localStorage.getItem(_LS_USER_SETTINGS)
+ if (s !== null) {
+ let settings = JSON.parse(s)
+ for (s in settings) {
+ Vue.set(this.user_settings, s, settings[s])
+ }
+ console.log(`loaded local user settings age ${((new Date().getTime()) - this.user_settings.locally_updated_at) / 1000} `)
+ }
+ if (((new Date().getTime()) - this.user_settings.locally_updated_at) > _STALE_TIME_IN_MS || !allow_cached_results) {
+ console.log('refreshing user settings from API')
+ let apiClient = new ApiApiFactory()
+ apiClient.retrieveUserPreference(localStorage.getItem('USER_ID')).then(r => {
+ for (s in r.data) {
+ if (!(s in this.user_settings) && s !== 'user') {
+ // dont load new keys if no default exists (to prevent forgetting to add defaults)
+ console.error(`API returned UserPreference key "${s}" which has no default in UserPreferenceStore.user_settings.`)
+ } else {
+ Vue.set(this.user_settings, s, r.data[s])
+ }
+ }
+ Vue.set(this.user_settings, 'locally_updated_at', new Date().getTime())
+ localStorage.setItem(_LS_USER_SETTINGS, JSON.stringify(this.user_settings))
+ }).catch(err => {
+ this.currently_updating = false
+ })
+ }
+
+ },
+ updateUserSettings: function () {
+ let apiClient = new ApiApiFactory()
+ apiClient.partialUpdateUserPreference(_USER_ID, this.user_settings).then(r => {
+ this.user_settings = r.data
+ Vue.set(this.user_settings, 'locally_updated_at', new Date().getTime())
+ localStorage.setItem(_LS_USER_SETTINGS, JSON.stringify(this.user_settings))
+ StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
+ }).catch(err => {
+ this.currently_updating = false
+ StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
+ })
+ },
+ // ----------------
+ // User Preferences (database settings stored in user preference model)
+ /**
+ * gets data from the store either directly or refreshes from API if data is considered stale
+ * @returns {UserPreference|*|Promise>}
+ */
+ getData: function () {
+ if (this.isStaleOrEmpty) {
+ return this.refreshFromAPI()
+ } else {
+ return this.data
+ }
+ },
+ /**
+ * get data from store. Does not use API, if store is not initialized returns null.
+ * @returns {null|UserPreference|*}
+ */
+ getStaleData: function () {
+ return this.data
+ },
+ /**
+ * checks if update timestamp is older than configured stale time interval
+ * @returns {boolean} true if data is considered stale and should be updated
+ */
+ isStale() {
+ return this.updated_at === null || ((new Date()) - this.updated_at) > _STALE_TIME_IN_MS;
+ },
+ /**
+ * checks if data of store is empty/not initialized
+ * @returns {boolean} true if store is empty
+ */
+ isEmpty() {
+ return this.data === null
+ },
+ /**
+ * checks if store is empty or data is considered stale, see isStale() and isEmpty()
+ * @returns {boolean}
+ */
+ isStaleOrEmpty() {
+ return this.isStale() || this.isEmpty()
+ },
+ /**
+ * refreshes store data if isStaleOrEmpty() is true
+ * @returns {Promise>} returns promise with data
+ */
+ updateIfStaleOrEmpty() {
+ if (this.isStaleOrEmpty) {
+ return this.refreshFromAPI()
+ }
+ },
+ /**
+ * refreshes store data from API
+ * @returns {Promise>} returns promise with data
+ */
+ refreshFromAPI() {
+ let apiClient = new ApiApiFactory()
+ if (!this.currently_updating) {
+ this.currently_updating = true
+ return apiClient.retrieveUserPreference(localStorage.getItem('USER_ID')).then(r => {
+ this.data = r.data
+ this.updated_at = new Date()
+ this.currently_updating = false
+
+ this.user_settings = r.data
+ this.user_settings_loaded_at = new Date()
+
+ return this.data
+ }).catch(err => {
+ this.currently_updating = false
+ })
+ }
+ },
+ },
+})
\ No newline at end of file