mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-04 21:58:54 -05:00
autosync
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- add alert at top if offline -->
|
<!-- add alert at top if offline -->
|
||||||
<!-- get autosync time from preferences and put fetching checked items on timer -->
|
<!-- get autosync time from preferences and put fetching checked items on timer -->
|
||||||
<!-- allow reordering or items -->
|
|
||||||
<div id="app" style="margin-bottom: 4vh">
|
<div id="app" style="margin-bottom: 4vh">
|
||||||
|
<b-alert :show="online" dismissible class="small float-up" variant="warning">{{ $t("OfflineAlert") }}</b-alert>
|
||||||
<div class="row float-top">
|
<div class="row float-top">
|
||||||
<div class="offset-md-10 col-md-2 no-gutter text-right">
|
<div class="offset-md-10 col-md-2 no-gutter text-right">
|
||||||
<b-button variant="link" class="px-0">
|
<b-button variant="link" class="px-0">
|
||||||
@@ -19,9 +19,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<!-- TODO add spinner -->
|
|
||||||
<div role="tablist" v-if="items && items.length > 0">
|
<div role="tablist" v-if="items && items.length > 0">
|
||||||
<!-- WARNING: all data in the table should be considered read-only, don't change any data through table bindings -->
|
|
||||||
<div class="row justify-content-md-center w-75" v-if="entrymode">
|
<div class="row justify-content-md-center w-75" v-if="entrymode">
|
||||||
<div class="col col-md-2 "><b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input></div>
|
<div class="col col-md-2 "><b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input></div>
|
||||||
<div class="col col-md-3">
|
<div class="col col-md-3">
|
||||||
@@ -74,12 +72,7 @@
|
|||||||
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
|
||||||
<!-- passing an array of values to the table grouped by Food -->
|
<!-- passing an array of values to the table grouped by Food -->
|
||||||
<div v-for="(entries, x) in Object.entries(s)" :key="x">
|
<div v-for="(entries, x) in Object.entries(s)" :key="x">
|
||||||
<ShoppingLineItem
|
<ShoppingLineItem :entries="entries[1]" :groupby="group_by" @open-context-menu="openContextMenu" @update-checkbox="updateChecked" />
|
||||||
:entries="entries[1]"
|
|
||||||
:groupby="group_by"
|
|
||||||
@open-context-menu="openContextMenu"
|
|
||||||
@update-checkbox="updateChecked"
|
|
||||||
></ShoppingLineItem>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +86,6 @@
|
|||||||
<table class="table w-75">
|
<table class="table w-75">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">ID</th>
|
|
||||||
<th scope="col">{{ $t("Meal_Plan") }}</th>
|
<th scope="col">{{ $t("Meal_Plan") }}</th>
|
||||||
<th scope="col">{{ $t("Recipe") }}</th>
|
<th scope="col">{{ $t("Recipe") }}</th>
|
||||||
<th scope="col">{{ $t("Servings") }}</th>
|
<th scope="col">{{ $t("Servings") }}</th>
|
||||||
@@ -101,7 +93,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tr v-for="r in Recipes" :key="r.list_recipe">
|
<tr v-for="r in Recipes" :key="r.list_recipe">
|
||||||
<td>{{ r.list_recipe }}</td>
|
|
||||||
<td>{{ r.recipe_mealplan.name }}</td>
|
<td>{{ r.recipe_mealplan.name }}</td>
|
||||||
<td>{{ r.recipe_mealplan.recipe_name }}</td>
|
<td>{{ r.recipe_mealplan.recipe_name }}</td>
|
||||||
<td class="block-inline">
|
<td class="block-inline">
|
||||||
@@ -230,8 +221,6 @@ import { StandardToasts, makeToast } from "@/utils/utils"
|
|||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
|
||||||
// or i'm capturing it incorrectly
|
|
||||||
name: "ShoppingListView",
|
name: "ShoppingListView",
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect },
|
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect },
|
||||||
@@ -240,7 +229,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
// this.Models and this.Actions inherited from ApiMixin
|
// this.Models and this.Actions inherited from ApiMixin
|
||||||
items: [],
|
items: [],
|
||||||
group_by: "category", //TODO create setting to switch group_by
|
group_by: "category",
|
||||||
group_by_choices: ["created_by", "category", "recipe"],
|
group_by_choices: ["created_by", "category", "recipe"],
|
||||||
supermarkets: [],
|
supermarkets: [],
|
||||||
shopping_categories: [],
|
shopping_categories: [],
|
||||||
@@ -248,13 +237,17 @@ export default {
|
|||||||
show_undefined_categories: true,
|
show_undefined_categories: true,
|
||||||
supermarket_categories_only: false,
|
supermarket_categories_only: false,
|
||||||
shopcat: null,
|
shopcat: null,
|
||||||
delay: 0, // user default
|
delay: 0,
|
||||||
|
autosync: 0,
|
||||||
|
autosync_id: undefined,
|
||||||
|
auto_sync_running: false,
|
||||||
show_delay: false,
|
show_delay: false,
|
||||||
show_modal: false,
|
show_modal: false,
|
||||||
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
|
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
|
||||||
loading: true,
|
loading: true,
|
||||||
entrymode: false,
|
entrymode: false,
|
||||||
new_item: { amount: 1, unit: undefined, food: undefined },
|
new_item: { amount: 1, unit: undefined, food: undefined },
|
||||||
|
online: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -341,22 +334,44 @@ export default {
|
|||||||
selected_supermarket(newVal, oldVal) {
|
selected_supermarket(newVal, oldVal) {
|
||||||
this.getShoppingList()
|
this.getShoppingList()
|
||||||
},
|
},
|
||||||
|
autosync(newVal, oldVal) {
|
||||||
|
clearInterval(this.autosync_id)
|
||||||
|
this.autosync_id = undefined
|
||||||
|
if (!newVal) {
|
||||||
|
window.removeEventListener("online", this.updateOnlineStatus)
|
||||||
|
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||||
|
return
|
||||||
|
} else if (oldVal === 0 && newVal > 0) {
|
||||||
|
console.log("adding listener")
|
||||||
|
window.addEventListener("online", this.updateOnlineStatus)
|
||||||
|
window.addEventListener("offline", this.updateOnlineStatus)
|
||||||
|
}
|
||||||
|
this.autosync_id = setInterval(() => {
|
||||||
|
if (this.online && !this.auto_sync_running) {
|
||||||
|
console.log("interval", this.online && !this.auto_sync_running)
|
||||||
|
this.auto_sync_running = true
|
||||||
|
this.getShoppingList(true)
|
||||||
|
}
|
||||||
|
}, this.autosync * 1000)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
// value is passed from lists.py
|
|
||||||
this.getShoppingList()
|
this.getShoppingList()
|
||||||
this.getSupermarkets()
|
this.getSupermarkets()
|
||||||
this.getShoppingCategories()
|
this.getShoppingCategories()
|
||||||
this.delay = getUserPreference("default_delay") || 4
|
this.delay = getUserPreference("default_delay") || 4
|
||||||
|
this.autosync = getUserPreference("shopping_auto_sync")
|
||||||
|
if (this.autosync) {
|
||||||
|
window.addEventListener("online", this.updateOnlineStatus)
|
||||||
|
window.addEventListener("offline", this.updateOnlineStatus)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
addItem() {
|
addItem() {
|
||||||
console.log(this.new_item)
|
|
||||||
let api = new ApiApiFactory()
|
let api = new ApiApiFactory()
|
||||||
api.createShoppingListEntry(this.new_item)
|
api.createShoppingListEntry(this.new_item)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
console.log(results)
|
|
||||||
if (results?.data) {
|
if (results?.data) {
|
||||||
this.items.push(results.data)
|
this.items.push(results.data)
|
||||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||||
@@ -436,21 +451,28 @@ export default {
|
|||||||
this.shopping_categories = result.data
|
this.shopping_categories = result.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getShoppingList: function(params = {}) {
|
getShoppingList: function(autosync = false) {
|
||||||
this.loading = true
|
let params = {}
|
||||||
params = {
|
params.supermarket = this.selected_supermarket
|
||||||
supermarket: this.selected_supermarket,
|
|
||||||
}
|
|
||||||
params.options = { query: { recent: 1 } }
|
params.options = { query: { recent: 1 } }
|
||||||
|
if (autosync) {
|
||||||
|
params.options.query["autosync"] = 1
|
||||||
|
} else {
|
||||||
|
this.loading = true
|
||||||
|
}
|
||||||
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
|
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
console.log(results.data)
|
if (!autosync) {
|
||||||
if (results.data?.length) {
|
if (results.data?.length) {
|
||||||
this.items = results.data
|
this.items = results.data
|
||||||
|
} else {
|
||||||
|
console.log("no data returned")
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
} else {
|
} else {
|
||||||
console.log("no data returned")
|
this.mergeShoppingList(results.data)
|
||||||
}
|
}
|
||||||
this.loading = false
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@@ -461,15 +483,11 @@ export default {
|
|||||||
let api = new ApiApiFactory()
|
let api = new ApiApiFactory()
|
||||||
api.listSupermarkets().then((result) => {
|
api.listSupermarkets().then((result) => {
|
||||||
this.supermarkets = result.data
|
this.supermarkets = result.data
|
||||||
console.log(this.supermarkets)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getThis: function(id) {
|
getThis: function(id) {
|
||||||
return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id })
|
return this.genericAPI(this.Models.SHOPPING_CATEGORY, this.Actions.FETCH, { id: id })
|
||||||
},
|
},
|
||||||
getRecipe: function(item) {
|
|
||||||
// change to get pop up card? maybe same for unit and food?
|
|
||||||
},
|
|
||||||
ignoreThis: function(item) {
|
ignoreThis: function(item) {
|
||||||
let food = {
|
let food = {
|
||||||
id: item?.[0]?.food.id ?? item.food.id,
|
id: item?.[0]?.food.id ?? item.food.id,
|
||||||
@@ -477,6 +495,18 @@ export default {
|
|||||||
}
|
}
|
||||||
this.updateFood(food, "ignore_shopping")
|
this.updateFood(food, "ignore_shopping")
|
||||||
},
|
},
|
||||||
|
mergeShoppingList: function(data) {
|
||||||
|
console.log(data)
|
||||||
|
this.items.map((x) =>
|
||||||
|
data.map((y) => {
|
||||||
|
if (y.id === x.id) {
|
||||||
|
x.checked = y.checked
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
this.auto_sync_running = false
|
||||||
|
},
|
||||||
moveEntry: function(e, item) {
|
moveEntry: function(e, item) {
|
||||||
if (!e) {
|
if (!e) {
|
||||||
makeToast(this.$t("Warning"), this.$t("NoCategory"), "warning")
|
makeToast(this.$t("Warning"), this.$t("NoCategory"), "warning")
|
||||||
@@ -536,7 +566,6 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err, err.response)
|
console.log(err, err.response)
|
||||||
|
|
||||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -546,15 +575,17 @@ export default {
|
|||||||
},
|
},
|
||||||
updateChecked: function(update) {
|
updateChecked: function(update) {
|
||||||
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
|
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
|
||||||
|
|
||||||
let promises = []
|
let promises = []
|
||||||
update.entries.forEach((x) => {
|
update.entries.forEach((x) => {
|
||||||
console.log({ id: x, checked: update.checked })
|
|
||||||
promises.push(this.saveThis({ id: x, checked: update.checked }, false))
|
promises.push(this.saveThis({ id: x, checked: update.checked }, false))
|
||||||
let item = this.items.filter((entry) => entry.id == x)[0]
|
let item = this.items.filter((entry) => entry.id == x)[0]
|
||||||
|
|
||||||
Vue.set(item, "checked", update.checked)
|
Vue.set(item, "checked", update.checked)
|
||||||
Vue.set(item, "completed_at", new Date().toISOString())
|
if (update.checked) {
|
||||||
|
Vue.set(item, "completed_at", new Date().toISOString())
|
||||||
|
} else {
|
||||||
|
Vue.set(item, "completed_at", undefined)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
Promise.all(promises).catch((err) => {
|
Promise.all(promises).catch((err) => {
|
||||||
console.log(err, err.response)
|
console.log(err, err.response)
|
||||||
@@ -597,6 +628,14 @@ export default {
|
|||||||
this.getShoppingList()
|
this.getShoppingList()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateOnlineStatus(e) {
|
||||||
|
const { type } = e
|
||||||
|
this.online = type === "online"
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener("online", this.updateOnlineStatus)
|
||||||
|
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -618,4 +657,8 @@ export default {
|
|||||||
padding-bottom: -3em;
|
padding-bottom: -3em;
|
||||||
margin-bottom: -3em;
|
margin-bottom: -3em;
|
||||||
}
|
}
|
||||||
|
.float-up {
|
||||||
|
padding-top: -3em;
|
||||||
|
margin-top: -3em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,8 +21,10 @@
|
|||||||
<input type="checkbox" class="text-right mx-3 mt-2" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
<input type="checkbox" class="text-right mx-3 mt-2" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-1">{{ formatAmount }}</div>
|
<div class="col-sm-3">
|
||||||
<div class="col col-md-1">{{ formatUnit }}</div>
|
<div v-if="Object.entries(formatAmount).length == 1">{{ Object.entries(formatAmount)[0][1] }}   {{ Object.entries(formatAmount)[0][0] }}</div>
|
||||||
|
<div class="small" v-else v-for="(x, i) in Object.entries(formatAmount)" :key="i">{{ x[1] }}   {{ x[0] }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{{ formatFood }} <span class="small text-muted">{{ formatHint }}</span>
|
{{ formatFood }} <span class="small text-muted">{{ formatHint }}</span>
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-link stn-sm m-0 p-0"
|
class="btn btn-link btn-sm m-0 p-0"
|
||||||
style="text-overflow: ellipsis;"
|
style="text-overflow: ellipsis;"
|
||||||
@click.stop="openRecipeCard($event, e)"
|
@click.stop="openRecipeCard($event, e)"
|
||||||
@mouseover="openRecipeCard($event, e)"
|
@mouseover="openRecipeCard($event, e)"
|
||||||
@@ -74,6 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-1">{{ formatOneAmount(e) }}</div>
|
<div class="col-sm-1">{{ formatOneAmount(e) }}</div>
|
||||||
<div class="col-sm-2">{{ formatOneUnit(e) }}</div>
|
<div class="col-sm-2">{{ formatOneUnit(e) }}</div>
|
||||||
|
|
||||||
<div class="col-sm-3">{{ formatOneFood(e) }}</div>
|
<div class="col-sm-3">{{ formatOneFood(e) }}</div>
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
@@ -136,7 +139,18 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
formatAmount: function() {
|
formatAmount: function() {
|
||||||
return this.formatOneAmount(this.entries[0])
|
let amount = {}
|
||||||
|
this.entries.forEach((entry) => {
|
||||||
|
let unit = entry?.unit?.name ?? "----"
|
||||||
|
if (entry.amount) {
|
||||||
|
if (amount[unit]) {
|
||||||
|
amount[unit] += entry.amount
|
||||||
|
} else {
|
||||||
|
amount[unit] = entry.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return amount
|
||||||
},
|
},
|
||||||
formatCategory: function() {
|
formatCategory: function() {
|
||||||
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
||||||
|
|||||||
Reference in New Issue
Block a user