This commit is contained in:
smilerz
2021-10-26 19:56:44 -05:00
parent 5c9f5e0e1a
commit 7c598720d0
9 changed files with 177 additions and 765 deletions

View File

@@ -1,6 +1,8 @@
<template>
<!-- add alert at top if offline -->
<!-- get autosync time from preferences and put fetching checked items on timer -->
<!-- allow reordering or items -->
<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="offset-md-10 col-md-2 no-gutter text-right">
<b-button variant="link" class="px-0">
@@ -11,15 +13,15 @@
</b-button>
</div>
</div>
<b-tabs content-class="mt-3">
<!-- shopping list tab -->
<b-tab :title="$t('ShoppingList')" active>
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("ShoppingList") }} </template>
<div class="container">
<div class="row">
<div class="col col-md-12">
<!-- TODO add spinner -->
<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="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">
@@ -72,7 +74,12 @@
<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 -->
<div v-for="(entries, x) in Object.entries(s)" :key="x">
<ShoppingLineItem :entries="entries[1]" :groupby="group_by" @open-context-menu="openContextMenu" @update-checkbox="updateChecked" />
<ShoppingLineItem
:entries="entries[1]"
:groupby="group_by"
@open-context-menu="openContextMenu"
@update-checkbox="updateChecked"
></ShoppingLineItem>
</div>
</div>
</div>
@@ -82,11 +89,11 @@
</div>
</div>
</b-tab>
<!-- recipe tab -->
<b-tab :title="$t('Recipes')">
<table class="table w-75">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">{{ $t("Meal_Plan") }}</th>
<th scope="col">{{ $t("Recipe") }}</th>
<th scope="col">{{ $t("Servings") }}</th>
@@ -94,6 +101,7 @@
</tr>
</thead>
<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.recipe_name }}</td>
<td class="block-inline">
@@ -105,124 +113,15 @@
</tr>
</table>
</b-tab>
<!-- settings tab -->
<b-tab :title="$t('Settings')">
<div class="row">
<div class="col col-md-4 ">
<b-card class="no-body">
<div class="row">
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.mealplan_autoadd_shopping" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">{{ $t("mealplan_autoadd_shopping_desc") }}</em>
</div>
</div>
<div v-if="settings.mealplan_autoadd_shopping">
<div class="row">
<div class="col col-md-6">{{ $t("mealplan_autoadd_shopping") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.mealplan_autoexclude_onhand" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">{{ $t("mealplan_autoadd_shopping_desc") }}</em>
</div>
</div>
</div>
<div v-if="settings.mealplan_autoadd_shopping">
<div class="row">
<div class="col col-md-6">{{ $t("mealplan_autoinclude_related") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.mealplan_autoinclude_related" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("mealplan_autoinclude_related_desc") }}
</em>
</div>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("shopping_share") }}</div>
<div class="col col-md-6 text-right">
<generic-multiselect
size="sm"
@change="
settings.shopping_share = $event.val
saveSettings()
"
:model="Models.USER"
:initial_selection="settings.shopping_share"
label="username"
:multiple="true"
:allow_create="false"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('User')"
/>
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">{{ $t("shopping_share_desc") }}</em>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("shopping_auto_sync") }}</div>
<div class="col col-md-6 text-right">
<input type="number" size="sm" v-model="settings.shopping_auto_sync" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("shopping_auto_sync_desc") }}
</em>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
<div class="col col-md-6 text-right">
<input type="checkbox" size="sm" v-model="settings.filter_to_supermarket" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("filter_to_supermarket_desc") }}
</em>
</div>
</div>
<div class="row">
<div class="col col-md-6">{{ $t("default_delay") }}</div>
<div class="col col-md-6 text-right">
<input type="number" size="sm" min="1" v-model="settings.default_delay" @change="saveSettings" />
</div>
</div>
<div class="row sm mb-3">
<div class="col">
<em class="small text-muted">
{{ $t("default_delay_desc") }}
</em>
</div>
</div>
</b-card>
</div>
<div class="col col-md-8">
<b-card class=" no-body">
put the supermarket stuff here<br />
-add supermarkets<br />
-add supermarket categories<br />
-sort supermarket categories<br />
</b-card>
</div>
</div>
These are the settings <br />-sort supermarket categories<br />
-add supermarket categories<br />
- add supermarkets autosync time<br />
autosync on/off<br />
always restrict supermarket to categories?<br />
when restricted or filterd - give visual indication<br />
how long to defer shopping - default tomorrow <br />
always override inheritance
</b-tab>
</b-tabs>
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
@@ -243,10 +142,9 @@
<b-form-checkbox v-model="supermarket_categories_only"></b-form-checkbox>
</b-form-group>
</div>
<div class="row " style="margin-top: 1vh;min-width:300px">
<div class="col-12 " style="text-align: right;">
<b-button size="sm" variant="primary" class="mx-1" @click="resetFilters">{{ $t("Reset") }} </b-button>
<b-button size="sm" variant="secondary" class="mr-3" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
<div class="row" style="margin-top: 1vh;min-width:300px">
<div class="col-12" style="text-align: right;">
<b-button size="sm" variant="secondary" style="margin-right:20px" @click="$root.$emit('bv::hide::popover')">{{ $t("Close") }} </b-button>
</div>
</div>
</b-popover>
@@ -332,6 +230,8 @@ import { StandardToasts, makeToast } from "@/utils/utils"
Vue.use(BootstrapVue)
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",
mixins: [ApiMixin],
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect },
@@ -340,7 +240,7 @@ export default {
return {
// this.Models and this.Actions inherited from ApiMixin
items: [],
group_by: "category",
group_by: "category", //TODO create setting to switch group_by
group_by_choices: ["created_by", "category", "recipe"],
supermarkets: [],
shopping_categories: [],
@@ -348,25 +248,13 @@ export default {
show_undefined_categories: true,
supermarket_categories_only: false,
shopcat: null,
delay: 0,
settings: {
shopping_auto_sync: 0,
default_delay: 4,
mealplan_autoadd_shopping: false,
mealplan_autoinclude_related: false,
mealplan_autoexclude_onhand: true,
filter_to_supermarket: false,
},
autosync_id: undefined,
auto_sync_running: false,
delay: 0, // user default
show_delay: false,
show_modal: false,
fields: ["checked", "amount", "category", "unit", "food", "recipe", "details"],
loading: true,
entrymode: false,
new_item: { amount: 1, unit: undefined, food: undefined },
online: true,
}
},
computed: {
@@ -401,16 +289,16 @@ export default {
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
}
let groups = { false: {}, true: {} } // force unchecked to always be first
const groups = { false: {}, true: {} } // force unchecked to always be first
if (this.selected_supermarket) {
let super_cats = this.supermarkets
.filter((x) => x.id === this.selected_supermarket)
.map((x) => x.category_to_supermarket)
.flat()
.map((x) => x.category.name)
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).forEach((cat) => {
groups["false"][cat.name] = {}
groups["true"][cat.name] = {}
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).foreach((cat) => {
groups.false[cat.name] = {}
groups.true[cat.name] = {}
})
} else {
this.shopping_categories.forEach((cat) => {
@@ -443,7 +331,7 @@ export default {
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
},
filterApplied() {
return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || (this.supermarket_categories_only && this.selected_supermarket)
return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || this.supermarket_categories_only
},
Recipes() {
return [...new Map(this.items.filter((x) => x.list_recipe).map((item) => [item["list_recipe"], item])).values()]
@@ -451,49 +339,24 @@ export default {
},
watch: {
selected_supermarket(newVal, oldVal) {
this.supermarket_categories_only = this.settings.filter_to_supermarket
},
"settings.filter_to_supermarket": function(newVal, oldVal) {
this.supermarket_categories_only = this.settings.filter_to_supermarket
},
"settings.shopping_auto_sync": function(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) {
window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus)
}
this.autosync_id = setInterval(() => {
if (this.online && !this.auto_sync_running) {
this.auto_sync_running = true
this.getShoppingList(true)
}
}, this.settings.shopping_auto_sync * 1000)
this.getShoppingList()
},
},
mounted() {
// value is passed from lists.py
this.getShoppingList()
this.getSupermarkets()
this.getShoppingCategories()
this.settings = getUserPreference()
this.delay = this.settings.default_delay || 4
this.supermarket_categories_only = this.settings.filter_to_supermarket
if (this.settings.shopping_auto_sync) {
window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus)
}
this.delay = getUserPreference("default_delay") || 4
},
methods: {
// this.genericAPI inherited from ApiMixin
addItem() {
console.log(this.new_item)
let api = new ApiApiFactory()
api.createShoppingListEntry(this.new_item)
.then((results) => {
console.log(results)
if (results?.data) {
this.items.push(results.data)
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
@@ -510,13 +373,6 @@ export default {
categoryName: function(value) {
return this.shopping_categories.filter((x) => x.id == value)[0]?.name ?? ""
},
resetFilters: function() {
this.selected_supermarket = undefined
this.supermarket_categories_only = this.settings.filter_to_supermarket
this.show_undefined_categories = true
this.group_by = "category"
this.show_delay = false
},
delayThis: function(item) {
let entries = []
let promises = []
@@ -580,45 +436,40 @@ export default {
this.shopping_categories = result.data
})
},
getShoppingList: function(autosync = false) {
let params = {}
params.supermarket = this.selected_supermarket
params.options = { query: { recent: 1 } }
if (autosync) {
params.options.query["autosync"] = 1
} else {
this.loading = true
getShoppingList: function(params = {}) {
this.loading = true
params = {
supermarket: this.selected_supermarket,
}
params.options = { query: { recent: 1 } }
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params)
.then((results) => {
if (!autosync) {
if (results.data?.length) {
this.items = results.data
} else {
console.log("no data returned")
}
this.loading = false
console.log(results.data)
if (results.data?.length) {
this.items = results.data
} else {
this.mergeShoppingList(results.data)
console.log("no data returned")
}
this.loading = false
})
.catch((err) => {
console.log(err)
if (!autosync) {
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
}
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
getSupermarkets: function() {
let api = new ApiApiFactory()
api.listSupermarkets().then((result) => {
this.supermarkets = result.data
console.log(this.supermarkets)
})
},
getThis: function(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) {
let food = {
id: item?.[0]?.food.id ?? item.food.id,
@@ -626,17 +477,6 @@ export default {
}
this.updateFood(food, "ignore_shopping")
},
mergeShoppingList: function(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) {
if (!e) {
makeToast(this.$t("Warning"), this.$t("NoCategory"), "warning")
@@ -671,17 +511,6 @@ export default {
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
this.$refs.menu.open(e, value)
},
saveSettings: function() {
let api = ApiApiFactory()
api.partialUpdateUserPreference(this.settings.user, this.settings)
.then((result) => {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
},
saveThis: function(thisItem, toast = true) {
let api = new ApiApiFactory()
if (!thisItem?.id) {
@@ -707,6 +536,7 @@ export default {
})
.catch((err) => {
console.log(err, err.response)
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
}
@@ -716,17 +546,15 @@ export default {
},
updateChecked: function(update) {
// when checking a sub item don't refresh the screen until all entries complete but change class to cross out
let promises = []
update.entries.forEach((x) => {
console.log({ id: x, checked: update.checked })
promises.push(this.saveThis({ id: x, checked: update.checked }, false))
let item = this.items.filter((entry) => entry.id == x)[0]
Vue.set(item, "checked", update.checked)
if (update.checked) {
Vue.set(item, "completed_at", new Date().toISOString())
} else {
Vue.set(item, "completed_at", undefined)
}
Vue.set(item, "completed_at", new Date().toISOString())
})
Promise.all(promises).catch((err) => {
console.log(err, err.response)
@@ -769,14 +597,6 @@ export default {
this.getShoppingList()
})
},
updateOnlineStatus(e) {
const { type } = e
this.online = type === "online"
},
beforeDestroy() {
window.removeEventListener("online", this.updateOnlineStatus)
window.removeEventListener("offline", this.updateOnlineStatus)
},
},
}
</script>
@@ -798,8 +618,4 @@ export default {
padding-bottom: -3em;
margin-bottom: -3em;
}
.float-up {
padding-top: -3em;
margin-top: -3em;
}
</style>