alpha shopping list

This commit is contained in:
smilerz
2021-10-27 11:51:14 -05:00
parent 4c506750de
commit 7c6a7ef6a4
7 changed files with 188 additions and 61 deletions

View File

@ -478,7 +478,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
model = UserPreference model = UserPreference
fields = ( fields = (
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand', 'default_delay' 'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket'
) )
help_texts = { help_texts = {
@ -489,14 +490,18 @@ class ShoppingPreferenceForm(forms.ModelForm):
), ),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'), 'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoexclude_onhand': _('When automatically adding a meal plan to the shopping list, exclude ingredients that are on hand.'), 'mealplan_autoexclude_onhand': _('When automatically adding a meal plan to the shopping list, exclude ingredients that are on hand.'),
'mealplan_autoinclude_related': _('When automatically adding a meal plan to the shopping list, include all related recipes.'),
'default_delay': _('Default number of hours to delay a shopping list entry.'), 'default_delay': _('Default number of hours to delay a shopping list entry.'),
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
} }
labels = { labels = {
'shopping_share': _('Share Shopping List'), 'shopping_share': _('Share Shopping List'),
'shopping_auto_sync': _('Autosync'), 'shopping_auto_sync': _('Autosync'),
'mealplan_autoadd_shopping': _('Auto Add Meal Plan'), 'mealplan_autoadd_shopping': _('Auto Add Meal Plan'),
'mealplan_autoexclude_onhand': _('Exclude On Hand'), 'mealplan_autoexclude_onhand': _('Exclude On Hand'),
'mealplan_autoinclude_related': _('Include Related'),
'default_delay': _('Default Delay Hours'), 'default_delay': _('Default Delay Hours'),
'filter_to_supermarket': _('Filter to Supermarket'),
} }
widgets = { widgets = {

View File

@ -152,5 +152,10 @@ class Migration(migrations.Migration):
name='shopping_recent_days', name='shopping_recent_days',
field=models.PositiveIntegerField(default=7), field=models.PositiveIntegerField(default=7),
), ),
migrations.AddField(
model_name='userpreference',
name='filter_to_supermarket',
field=models.BooleanField(default=False),
),
migrations.RunPython(copy_values_to_sle), migrations.RunPython(copy_values_to_sle),
] ]

View File

@ -329,6 +329,7 @@ class UserPreference(models.Model, PermissionModelMixin):
mealplan_autoadd_shopping = models.BooleanField(default=False) mealplan_autoadd_shopping = models.BooleanField(default=False)
mealplan_autoexclude_onhand = models.BooleanField(default=True) mealplan_autoexclude_onhand = models.BooleanField(default=True)
mealplan_autoinclude_related = models.BooleanField(default=True) mealplan_autoinclude_related = models.BooleanField(default=True)
filter_to_supermarket = models.BooleanField(default=False)
default_delay = models.IntegerField(default=4) default_delay = models.IntegerField(default=4)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)

View File

@ -162,7 +162,8 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
fields = ( fields = (
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'user', 'theme', 'nav_color', 'default_unit', 'default_page',
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals', 'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay' 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share'
) )

View File

@ -385,6 +385,7 @@ def user_settings(request):
up.mealplan_autoexclude_onhand = shopping_form.cleaned_data['mealplan_autoexclude_onhand'] up.mealplan_autoexclude_onhand = shopping_form.cleaned_data['mealplan_autoexclude_onhand']
up.mealplan_autoinclude_related = shopping_form.cleaned_data['mealplan_autoinclude_related'] up.mealplan_autoinclude_related = shopping_form.cleaned_data['mealplan_autoinclude_related']
up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync'] up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync']
up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket']
up.default_delay = shopping_form.cleaned_data['default_delay'] up.default_delay = shopping_form.cleaned_data['default_delay']
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL: if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL

View File

@ -1,8 +1,6 @@
<template> <template>
<!-- add alert at top if offline -->
<!-- get autosync time from preferences and put fetching checked items on timer -->
<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> <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">
@ -13,7 +11,9 @@
</b-button> </b-button>
</div> </div>
</div> </div>
<b-tabs content-class="mt-3"> <b-tabs content-class="mt-3">
<!-- shopping list tab -->
<b-tab :title="$t('ShoppingList')" active> <b-tab :title="$t('ShoppingList')" active>
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("ShoppingList") }} </template> <template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("ShoppingList") }} </template>
<div class="container"> <div class="container">
@ -82,6 +82,7 @@
</div> </div>
</div> </div>
</b-tab> </b-tab>
<!-- recipe tab -->
<b-tab :title="$t('Recipes')"> <b-tab :title="$t('Recipes')">
<table class="table w-75"> <table class="table w-75">
<thead> <thead>
@ -104,15 +105,124 @@
</tr> </tr>
</table> </table>
</b-tab> </b-tab>
<!-- settings tab -->
<b-tab :title="$t('Settings')"> <b-tab :title="$t('Settings')">
These are the settings <br />-sort supermarket categories<br /> <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 /> -add supermarket categories<br />
- add supermarkets autosync time<br /> -sort supermarket categories<br />
autosync on/off<br /> </b-card>
always restrict supermarket to categories?<br /> </div>
when restricted or filterd - give visual indication<br /> </div>
how long to defer shopping - default tomorrow <br />
always override inheritance
</b-tab> </b-tab>
</b-tabs> </b-tabs>
<b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')"> <b-popover target="id_filters_button" triggers="click" placement="bottomleft" :title="$t('Filters')">
@ -135,7 +245,8 @@
</div> </div>
<div class="row " style="margin-top: 1vh;min-width:300px"> <div class="row " style="margin-top: 1vh;min-width:300px">
<div class="col-12 " style="text-align: right;"> <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> <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> </div>
</div> </div>
</b-popover> </b-popover>
@ -238,7 +349,15 @@ export default {
supermarket_categories_only: false, supermarket_categories_only: false,
shopcat: null, shopcat: null,
delay: 0, delay: 0,
autosync: 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, autosync_id: undefined,
auto_sync_running: false, auto_sync_running: false,
show_delay: false, show_delay: false,
@ -282,16 +401,16 @@ export default {
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category) shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
} }
const groups = { false: {}, true: {} } // force unchecked to always be first let groups = { false: {}, true: {} } // force unchecked to always be first
if (this.selected_supermarket) { if (this.selected_supermarket) {
let super_cats = this.supermarkets let super_cats = this.supermarkets
.filter((x) => x.id === this.selected_supermarket) .filter((x) => x.id === this.selected_supermarket)
.map((x) => x.category_to_supermarket) .map((x) => x.category_to_supermarket)
.flat() .flat()
.map((x) => x.category.name) .map((x) => x.category.name)
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).foreach((cat) => { new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).forEach((cat) => {
groups.false[cat.name] = {} groups["false"][cat.name] = {}
groups.true[cat.name] = {} groups["true"][cat.name] = {}
}) })
} else { } else {
this.shopping_categories.forEach((cat) => { this.shopping_categories.forEach((cat) => {
@ -324,7 +443,7 @@ export default {
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
}, },
filterApplied() { filterApplied() {
return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || this.supermarket_categories_only return (this.itemsDelayed && !this.show_delay) || !this.show_undefined_categories || (this.supermarket_categories_only && this.selected_supermarket)
}, },
Recipes() { Recipes() {
return [...new Map(this.items.filter((x) => x.list_recipe).map((item) => [item["list_recipe"], item])).values()] return [...new Map(this.items.filter((x) => x.list_recipe).map((item) => [item["list_recipe"], item])).values()]
@ -332,9 +451,12 @@ export default {
}, },
watch: { watch: {
selected_supermarket(newVal, oldVal) { selected_supermarket(newVal, oldVal) {
this.getShoppingList() this.supermarket_categories_only = this.settings.filter_to_supermarket
}, },
autosync(newVal, oldVal) { "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) clearInterval(this.autosync_id)
this.autosync_id = undefined this.autosync_id = undefined
if (!newVal) { if (!newVal) {
@ -342,26 +464,26 @@ export default {
window.removeEventListener("offline", this.updateOnlineStatus) window.removeEventListener("offline", this.updateOnlineStatus)
return return
} else if (oldVal === 0 && newVal > 0) { } else if (oldVal === 0 && newVal > 0) {
console.log("adding listener")
window.addEventListener("online", this.updateOnlineStatus) window.addEventListener("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus) window.addEventListener("offline", this.updateOnlineStatus)
} }
this.autosync_id = setInterval(() => { this.autosync_id = setInterval(() => {
if (this.online && !this.auto_sync_running) { if (this.online && !this.auto_sync_running) {
console.log("interval", this.online && !this.auto_sync_running)
this.auto_sync_running = true this.auto_sync_running = true
this.getShoppingList(true) this.getShoppingList(true)
} }
}, this.autosync * 1000) }, this.settings.shopping_auto_sync * 1000)
}, },
}, },
mounted() { mounted() {
this.getShoppingList() this.getShoppingList()
this.getSupermarkets() this.getSupermarkets()
this.getShoppingCategories() this.getShoppingCategories()
this.delay = getUserPreference("default_delay") || 4
this.autosync = getUserPreference("shopping_auto_sync") this.settings = getUserPreference()
if (this.autosync) { 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("online", this.updateOnlineStatus)
window.addEventListener("offline", this.updateOnlineStatus) window.addEventListener("offline", this.updateOnlineStatus)
} }
@ -388,6 +510,13 @@ export default {
categoryName: function(value) { categoryName: function(value) {
return this.shopping_categories.filter((x) => x.id == value)[0]?.name ?? "" 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) { delayThis: function(item) {
let entries = [] let entries = []
let promises = [] let promises = []
@ -476,7 +605,9 @@ export default {
}) })
.catch((err) => { .catch((err) => {
console.log(err) console.log(err)
if (!autosync) {
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH) StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
}
}) })
}, },
getSupermarkets: function() { getSupermarkets: function() {
@ -496,7 +627,6 @@ export default {
this.updateFood(food, "ignore_shopping") this.updateFood(food, "ignore_shopping")
}, },
mergeShoppingList: function(data) { mergeShoppingList: function(data) {
console.log(data)
this.items.map((x) => this.items.map((x) =>
data.map((y) => { data.map((y) => {
if (y.id === x.id) { if (y.id === x.id) {
@ -541,6 +671,17 @@ export default {
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
this.$refs.menu.open(e, value) 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) { saveThis: function(thisItem, toast = true) {
let api = new ApiApiFactory() let api = new ApiApiFactory()
if (!thisItem?.id) { if (!thisItem?.id) {

View File

@ -74,18 +74,7 @@ export class Models {
// REQUIRED: unordered array of fields that can be set during create // REQUIRED: unordered array of fields that can be set during create
create: { create: {
// if not defined partialUpdate will use the same parameters, prepending 'id' // if not defined partialUpdate will use the same parameters, prepending 'id'
params: [ params: [["name", "description", "recipe", "ignore_shopping", "supermarket_category", "on_hand", "inherit", "ignore_inherit"]],
[
"name",
"description",
"recipe",
"ignore_shopping",
"supermarket_category",
"on_hand",
"inherit",
"ignore_inherit",
],
],
form: { form: {
name: { name: {
@ -418,23 +407,7 @@ export class Models {
name: i18n.t("Recipe"), name: i18n.t("Recipe"),
apiName: "Recipe", apiName: "Recipe",
list: { list: {
params: [ params: ["query", "keywords", "foods", "units", "rating", "books", "keywordsOr", "foodsOr", "booksOr", "internal", "random", "_new", "page", "pageSize", "options"],
"query",
"keywords",
"foods",
"units",
"rating",
"books",
"keywordsOr",
"foodsOr",
"booksOr",
"internal",
"random",
"_new",
"page",
"pageSize",
"options",
],
// 'config': { // 'config': {
// 'foods': {'type': 'string'}, // 'foods': {'type': 'string'},
// 'keywords': {'type': 'string'}, // 'keywords': {'type': 'string'},