mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 12:18:45 -05:00
basic shopping view in MealPlanEditor
This commit is contained in:
@@ -16,6 +16,7 @@ import PIL.Image
|
|||||||
import redis
|
import redis
|
||||||
import requests
|
import requests
|
||||||
from PIL import UnidentifiedImageError
|
from PIL import UnidentifiedImageError
|
||||||
|
from PIL.ImImagePlugin import number
|
||||||
from PIL.features import check
|
from PIL.features import check
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
@@ -1361,6 +1362,9 @@ class PropertyViewSet(LoggingMixin, viewsets.ModelViewSet):
|
|||||||
return self.queryset.filter(space=self.request.space)
|
return self.queryset.filter(space=self.request.space)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(list=extend_schema(parameters=[
|
||||||
|
OpenApiParameter(name='mealplan', description=_('Returns only entries associated with the given mealplan id'), type=int)
|
||||||
|
]))
|
||||||
class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||||
queryset = ShoppingListRecipe.objects
|
queryset = ShoppingListRecipe.objects
|
||||||
serializer_class = ShoppingListRecipeSerializer
|
serializer_class = ShoppingListRecipeSerializer
|
||||||
@@ -1369,6 +1373,14 @@ class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
self.queryset = self.queryset.filter(Q(entries__space=self.request.space) | Q(recipe__space=self.request.space))
|
self.queryset = self.queryset.filter(Q(entries__space=self.request.space) | Q(recipe__space=self.request.space))
|
||||||
|
|
||||||
|
# TODO implement test for this
|
||||||
|
if not self.detail:
|
||||||
|
mealplan = self.request.query_params.get('mealplan', None)
|
||||||
|
|
||||||
|
if mealplan is not None:
|
||||||
|
self.queryset = self.queryset.filter(mealplan_id=mealplan)
|
||||||
|
|
||||||
return self.queryset.filter(Q(entries__isnull=True)
|
return self.queryset.filter(Q(entries__isnull=True)
|
||||||
| Q(entries__created_by=self.request.user)
|
| Q(entries__created_by=self.request.user)
|
||||||
| Q(entries__created_by__in=list(self.request.user.get_shopping_share()))).distinct().all()
|
| Q(entries__created_by__in=list(self.request.user.get_shopping_share()))).distinct().all()
|
||||||
@@ -1405,6 +1417,7 @@ class ShoppingListRecipeViewSet(LoggingMixin, viewsets.ModelViewSet):
|
|||||||
OpenApiParameter(name='updated_after',
|
OpenApiParameter(name='updated_after',
|
||||||
description=_('Returns only elements updated after the given timestamp in ISO 8601 format.'),
|
description=_('Returns only elements updated after the given timestamp in ISO 8601 format.'),
|
||||||
type=datetime.datetime),
|
type=datetime.datetime),
|
||||||
|
OpenApiParameter(name='mealplan', description=_('Returns only entries associated with the given mealplan id'), type=int)
|
||||||
]))
|
]))
|
||||||
class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
|
class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
@@ -1435,6 +1448,7 @@ class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
|
|||||||
).distinct().all()
|
).distinct().all()
|
||||||
|
|
||||||
updated_after = self.request.query_params.get('updated_after', None)
|
updated_after = self.request.query_params.get('updated_after', None)
|
||||||
|
mealplan = self.request.query_params.get('mealplan', None)
|
||||||
|
|
||||||
if not self.detail:
|
if not self.detail:
|
||||||
# to keep the endpoint small, only return entries as old as user preference recent days
|
# to keep the endpoint small, only return entries as old as user preference recent days
|
||||||
@@ -1442,10 +1456,12 @@ class ShoppingListEntryViewSet(LoggingMixin, viewsets.ModelViewSet):
|
|||||||
week_ago = today_start - datetime.timedelta(days=min(self.request.user.userpreference.shopping_recent_days, 14))
|
week_ago = today_start - datetime.timedelta(days=min(self.request.user.userpreference.shopping_recent_days, 14))
|
||||||
self.queryset = self.queryset.filter((Q(checked=False) | Q(completed_at__gte=week_ago)))
|
self.queryset = self.queryset.filter((Q(checked=False) | Q(completed_at__gte=week_ago)))
|
||||||
|
|
||||||
|
if mealplan is not None:
|
||||||
|
self.queryset = self.queryset.filter(list_recipe__mealplan_id=mealplan)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if updated_after:
|
if updated_after:
|
||||||
updated_after = parse_datetime(updated_after)
|
updated_after = parse_datetime(updated_after)
|
||||||
print('adding filter updated_after', updated_after)
|
|
||||||
self.queryset = self.queryset.filter(updated_at__gte=updated_after)
|
self.queryset = self.queryset.filter(updated_at__gte=updated_after)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -1588,9 +1604,8 @@ class AutomationViewSet(LoggingMixin, StandardFilterModelViewSet):
|
|||||||
return self.queryset.filter(space=self.request.space).all()
|
return self.queryset.filter(space=self.request.space).all()
|
||||||
|
|
||||||
|
|
||||||
# TODO explain what internal_note is for
|
|
||||||
@extend_schema_view(list=extend_schema(parameters=[
|
@extend_schema_view(list=extend_schema(parameters=[
|
||||||
OpenApiParameter(name='internal_note', description=_('I have no idea what internal_note is for.'), type=str)
|
OpenApiParameter(name='internal_note', description=_('Text field to store data that gets carried over to the UserSpace created from the InviteLink'), type=str)
|
||||||
]))
|
]))
|
||||||
class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
|
class InviteLinkViewSet(LoggingMixin, StandardFilterModelViewSet):
|
||||||
queryset = InviteLink.objects
|
queryset = InviteLink.objects
|
||||||
@@ -1806,7 +1821,7 @@ class ImageToRecipeView(APIView):
|
|||||||
"""
|
"""
|
||||||
serializer = ImportImageSerializer(data=request.data, partial=True)
|
serializer = ImportImageSerializer(data=request.data, partial=True)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
#generativeai.configure(api_key=GOOGLE_AI_API_KEY)
|
# generativeai.configure(api_key=GOOGLE_AI_API_KEY)
|
||||||
|
|
||||||
# model = generativeai.GenerativeModel('gemini-1.5-flash-latest')
|
# model = generativeai.GenerativeModel('gemini-1.5-flash-latest')
|
||||||
# img = PIL.Image.open('')
|
# img = PIL.Image.open('')
|
||||||
|
|||||||
@@ -7,14 +7,13 @@
|
|||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex-grow-1 p-2">
|
<div class="flex-grow-1 p-2">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="d-flex flex-column pr-2">
|
<div class="d-flex flex-column pr-2">
|
||||||
<span v-for="[i, a] in amounts" v-bind:key="a.key">
|
<span v-for="[i, a] in amounts" v-bind:key="a.key">
|
||||||
<span>
|
<span>
|
||||||
<i class="fas fa-check text-success fa-fw" v-if="a.checked"></i>
|
<i class="fas fa-check text-success fa-fw" v-if="a.checked"></i>
|
||||||
<i class="fas fa-clock-rotate-left text-info fa-fw" v-if="a.delayed"></i> <b>
|
<i class="fas fa-clock-rotate-left text-info fa-fw" v-if="a.delayed"></i> <b>
|
||||||
<span :class="{'text-disabled': a.checked || a.delayed}">
|
<span :class="{'text-disabled': a.checked || a.delayed}">
|
||||||
{{ $n(a.amount) }}
|
{{ $n(a.amount) }}
|
||||||
<span v-if="a.unit">{{ a.unit.name }}</span>
|
<span v-if="a.unit">{{ a.unit.name }}</span>
|
||||||
@@ -25,7 +24,7 @@
|
|||||||
<br/>
|
<br/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column flex-grow-1 align-self-center" >
|
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||||
{{ shoppingListFood.food.name }} <br/>
|
{{ shoppingListFood.food.name }} <br/>
|
||||||
<span v-if="infoRow"><small class="text-disabled">{{ infoRow }}</small></span>
|
<span v-if="infoRow"><small class="text-disabled">{{ infoRow }}</small></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,8 +61,12 @@ import {isDelayed, isShoppingListFoodDelayed} from "@/utils/logic_utils";
|
|||||||
const emit = defineEmits(['clicked'])
|
const emit = defineEmits(['clicked'])
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
entries: {type: Array as PropType<Array<ShoppingListEntry>>, required: true},
|
|
||||||
shoppingListFood: {type: {} as PropType<IShoppingListFood>, required: true},
|
shoppingListFood: {type: {} as PropType<IShoppingListFood>, required: true},
|
||||||
|
hideInfoRow: {type: Boolean, default: false}
|
||||||
|
})
|
||||||
|
|
||||||
|
const entries = computed(() => {
|
||||||
|
return Array.from(props.shoppingListFood.entries.values())
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +74,7 @@ const props = defineProps({
|
|||||||
*/
|
*/
|
||||||
const itemContainerId = computed(() => {
|
const itemContainerId = computed(() => {
|
||||||
let id = 'id_sli_'
|
let id = 'id_sli_'
|
||||||
for (let i in props.entries) {
|
for (let i in entries.value) {
|
||||||
id += i + '_'
|
id += i + '_'
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
@@ -82,8 +85,8 @@ const itemContainerId = computed(() => {
|
|||||||
* tests if all entries of the given food are checked
|
* tests if all entries of the given food are checked
|
||||||
*/
|
*/
|
||||||
const isChecked = computed(() => {
|
const isChecked = computed(() => {
|
||||||
for (let i in props.entries) {
|
for (let i in entries.value) {
|
||||||
if (!props.entries[i].checked) {
|
if (!entries.value[i].checked) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +104,7 @@ const isShoppingLineDelayed = computed(() => {
|
|||||||
* style action button depending on if all items are checked or not
|
* style action button depending on if all items are checked or not
|
||||||
*/
|
*/
|
||||||
const actionButtonIcon = computed(() => {
|
const actionButtonIcon = computed(() => {
|
||||||
if (isChecked.value){
|
if (isChecked.value) {
|
||||||
return 'fa-solid fa-plus'
|
return 'fa-solid fa-plus'
|
||||||
}
|
}
|
||||||
return 'fa-solid fa-check'
|
return 'fa-solid fa-check'
|
||||||
@@ -116,8 +119,8 @@ const actionButtonIcon = computed(() => {
|
|||||||
const amounts = computed((): Map<number, ShoppingLineAmount> => {
|
const amounts = computed((): Map<number, ShoppingLineAmount> => {
|
||||||
let unitAmounts = new Map<number, ShoppingLineAmount>()
|
let unitAmounts = new Map<number, ShoppingLineAmount>()
|
||||||
|
|
||||||
for (let i in props.entries) {
|
for (let i in entries.value) {
|
||||||
let e = props.entries[i]
|
let e = entries.value[i]
|
||||||
|
|
||||||
if (!e.checked && !isDelayed(e)
|
if (!e.checked && !isDelayed(e)
|
||||||
|| (e.checked && useUserPreferenceStore().deviceSettings.shopping_show_checked_entries)
|
|| (e.checked && useUserPreferenceStore().deviceSettings.shopping_show_checked_entries)
|
||||||
@@ -147,15 +150,22 @@ const amounts = computed((): Map<number, ShoppingLineAmount> => {
|
|||||||
return unitAmounts
|
return unitAmounts
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* compute the second (info) row of the line item based on the entries and the device settings
|
||||||
|
*/
|
||||||
const infoRow = computed(() => {
|
const infoRow = computed(() => {
|
||||||
|
if(props.hideInfoRow){
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
let info_row = []
|
let info_row = []
|
||||||
|
|
||||||
let authors = []
|
let authors = []
|
||||||
let recipes = []
|
let recipes = []
|
||||||
let meal_pans = []
|
let meal_pans = []
|
||||||
|
|
||||||
for (let i in props.entries) {
|
for (let i in entries.value) {
|
||||||
let e = props.entries[i]
|
let e = entries.value[i]
|
||||||
|
|
||||||
if (authors.indexOf(e.createdBy.displayName) === -1) {
|
if (authors.indexOf(e.createdBy.displayName) === -1) {
|
||||||
authors.push(e.createdBy.displayName)
|
authors.push(e.createdBy.displayName)
|
||||||
@@ -203,7 +213,7 @@ function setFoodIgnoredAndChecked(food: Food) {
|
|||||||
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
useShoppingStore().setEntriesCheckedState(props.entries, true, false)
|
useShoppingStore().setEntriesCheckedState(entries.value, true, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
|
||||||
<template v-for="[i, value] in category.foods" :key="value.food.id">
|
<template v-for="[i, value] in category.foods" :key="value.food.id">
|
||||||
<shopping-line-item :shopping-list-food="value" :entries="Array.from(value.entries.values())"
|
<shopping-line-item :shopping-list-food="value"
|
||||||
@clicked="() => {shoppingLineItemDialog = true; shoppingLineItemDialogFood = value;}"></shopping-line-item>
|
@clicked="() => {shoppingLineItemDialog = true; shoppingLineItemDialogFood = value;}"></shopping-line-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -8,54 +8,89 @@
|
|||||||
:is-update="isUpdate()"
|
:is-update="isUpdate()"
|
||||||
:model-class="modelClass"
|
:model-class="modelClass"
|
||||||
:object-name="editingObjName()">
|
:object-name="editingObjName()">
|
||||||
|
|
||||||
|
<v-tabs v-model="tab" :disabled="loading" grow>
|
||||||
|
<v-tab prepend-icon="$mealplan" value="plan">{{ $t('Meal_Plan') }}</v-tab>
|
||||||
|
<v-tab prepend-icon="$shopping" value="shopping">{{ $t('Shopping_list') }}</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form :disabled="loading">
|
|
||||||
|
|
||||||
<v-row>
|
<v-tabs-window v-model="tab">
|
||||||
<v-col cols="12" md="6">
|
<v-tabs-window-item value="plan">
|
||||||
<v-text-field :label="$t('Title')" v-model="editingObj.title"></v-text-field>
|
<v-form :disabled="loading">
|
||||||
<v-date-input
|
|
||||||
v-model="dateRangeValue"
|
|
||||||
:label="$t('Date')"
|
|
||||||
multiple="range"
|
|
||||||
prepend-icon=""
|
|
||||||
prepend-inner-icon="$calendar"
|
|
||||||
></v-date-input>
|
|
||||||
|
|
||||||
<v-input>
|
<v-row>
|
||||||
<v-btn-group elevation="1" class="w-100" divided border>
|
<v-col cols="12" md="6">
|
||||||
<v-btn class="w-25" @click="adjustDateRangeLength(dateRangeValue,-1); updateDate()"><i class="fa-solid fa-minus"></i></v-btn>
|
<v-text-field :label="$t('Title')" v-model="editingObj.title"></v-text-field>
|
||||||
<v-btn class="w-25" @click="dateRangeValue = shiftDateRange(dateRangeValue, -1); updateDate()"><i class="fa-solid fa-angles-left"></i></v-btn>
|
<v-date-input
|
||||||
<v-btn class="w-25" @click="dateRangeValue = shiftDateRange(dateRangeValue, +1); updateDate()"><i class="fa-solid fa-angles-right"></i></v-btn>
|
v-model="dateRangeValue"
|
||||||
<v-btn class="w-25" @click="adjustDateRangeLength(dateRangeValue,+1); updateDate()"><i class="fa-solid fa-plus"></i></v-btn>
|
:label="$t('Date')"
|
||||||
</v-btn-group>
|
multiple="range"
|
||||||
</v-input>
|
prepend-icon=""
|
||||||
|
prepend-inner-icon="$calendar"
|
||||||
|
></v-date-input>
|
||||||
|
|
||||||
<ModelSelect model="MealType" :allow-create="true" v-model="editingObj.mealType"></ModelSelect>
|
<v-input>
|
||||||
<v-number-input control-variant="split" :min="0" v-model="editingObj.servings" :label="$t('Servings')"></v-number-input>
|
<v-btn-group elevation="1" class="w-100" divided border>
|
||||||
<ModelSelect model="User" :allow-create="false" v-model="editingObj.shared" item-label="displayName" mode="tags"></ModelSelect>
|
<v-btn class="w-25" @click="adjustDateRangeLength(dateRangeValue,-1); updateDate()"><i class="fa-solid fa-minus"></i></v-btn>
|
||||||
</v-col>
|
<v-btn class="w-25" @click="dateRangeValue = shiftDateRange(dateRangeValue, -1); updateDate()"><i class="fa-solid fa-angles-left"></i>
|
||||||
<v-col cols="12" md="6">
|
</v-btn>
|
||||||
<ModelSelect model="Recipe" v-model="editingObj.recipe"
|
<v-btn class="w-25" @click="dateRangeValue = shiftDateRange(dateRangeValue, +1); updateDate()"><i class="fa-solid fa-angles-right"></i>
|
||||||
@update:modelValue="editingObj.servings = editingObj.recipe ? editingObj.recipe.servings : 1"></ModelSelect>
|
</v-btn>
|
||||||
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>-->
|
<v-btn class="w-25" @click="adjustDateRangeLength(dateRangeValue,+1); updateDate()"><i class="fa-solid fa-plus"></i></v-btn>
|
||||||
<!--TODO create days input with +/- synced to date -->
|
</v-btn-group>
|
||||||
<recipe-card :recipe="editingObj.recipe" v-if="editingObj && editingObj.recipe"></recipe-card>
|
</v-input>
|
||||||
</v-col>
|
|
||||||
</v-row>
|
<ModelSelect model="MealType" :allow-create="true" v-model="editingObj.mealType"></ModelSelect>
|
||||||
<v-row dense>
|
<v-number-input control-variant="split" :min="0" v-model="editingObj.servings" :label="$t('Servings')"></v-number-input>
|
||||||
<v-col cols="12" md="6">
|
<ModelSelect model="User" :allow-create="false" v-model="editingObj.shared" item-label="displayName" mode="tags"></ModelSelect>
|
||||||
<v-textarea :label="$t('Note')" v-model="editingObj.note" rows="3"></v-textarea>
|
</v-col>
|
||||||
</v-col>
|
<v-col cols="12" md="6">
|
||||||
<v-col cols="12" md="6" v-if="!isUpdate()">
|
<ModelSelect model="Recipe" v-model="editingObj.recipe"
|
||||||
<v-checkbox :label="$t('AddToShopping')" v-model="editingObj.addshopping" hide-details></v-checkbox>
|
@update:modelValue="editingObj.servings = editingObj.recipe ? editingObj.recipe.servings : 1"></ModelSelect>
|
||||||
<!-- <v-checkbox :label="$t('review_shopping')" v-model="addToShopping" hide-details></v-checkbox>-->
|
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>-->
|
||||||
</v-col>
|
<!--TODO create days input with +/- synced to date -->
|
||||||
</v-row>
|
<recipe-card :recipe="editingObj.recipe" v-if="editingObj && editingObj.recipe"></recipe-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-textarea :label="$t('Note')" v-model="editingObj.note" rows="3"></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6" v-if="!isUpdate()">
|
||||||
|
<v-checkbox :label="$t('AddToShopping')" v-model="editingObj.addshopping" hide-details></v-checkbox>
|
||||||
|
<!-- <v-checkbox :label="$t('review_shopping')" v-model="addToShopping" hide-details></v-checkbox>-->
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="shopping">
|
||||||
|
<v-text-field :label="$t('Shopping_input_placeholder')" density="compact" @keyup.enter="addIngredient()" v-model="ingredientInput" hide-details>
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
density="comfortable"
|
||||||
|
@click="addIngredient()"
|
||||||
|
:icon="ingredientInputIcon"
|
||||||
|
color="create"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
|
||||||
|
<v-progress-linear class="mt-2" indeterminate v-if="useShoppingStore().currentlyUpdating"></v-progress-linear>
|
||||||
|
|
||||||
|
<v-list v-if="editingObj.id">
|
||||||
|
<shopping-line-item
|
||||||
|
v-for="slf in useShoppingStore().getMealPlanEntries(editingObj.id)"
|
||||||
|
:shopping-list-food="slf"
|
||||||
|
hide-info-row
|
||||||
|
></shopping-line-item>
|
||||||
|
</v-list>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
</v-tabs-window>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</model-editor-base>
|
</model-editor-base>
|
||||||
|
|
||||||
@@ -64,7 +99,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {onMounted, PropType, ref} from "vue";
|
import {onMounted, PropType, ref} from "vue";
|
||||||
import {ApiApi, MealPlan, MealType} from "@/openapi";
|
import {ApiApi, MealPlan, MealType, ShoppingListEntry} from "@/openapi";
|
||||||
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
import ModelEditorBase from "@/components/model_editors/ModelEditorBase.vue";
|
||||||
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
import {useModelEditorFunctions} from "@/composables/useModelEditorFunctions";
|
||||||
import {DateTime} from "luxon";
|
import {DateTime} from "luxon";
|
||||||
@@ -74,7 +109,10 @@ import ModelSelect from "@/components/inputs/ModelSelect.vue";
|
|||||||
import RecipeCard from "@/components/display/RecipeCard.vue";
|
import RecipeCard from "@/components/display/RecipeCard.vue";
|
||||||
import {VDateInput} from "vuetify/labs/VDateInput";
|
import {VDateInput} from "vuetify/labs/VDateInput";
|
||||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||||
import {MessageType, useMessageStore} from "@/stores/MessageStore";
|
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
|
||||||
|
import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue";
|
||||||
|
import {IShoppingListFood} from "@/types/Shopping";
|
||||||
|
import {useShoppingStore} from "@/stores/ShoppingStore";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {type: {} as PropType<MealPlan>, required: false, default: null},
|
item: {type: {} as PropType<MealPlan>, required: false, default: null},
|
||||||
@@ -87,14 +125,18 @@ const emit = defineEmits(['create', 'save', 'delete', 'close'])
|
|||||||
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, applyItemDefaults, loading, editingObj, modelClass} = useModelEditorFunctions<MealPlan>('MealPlan', emit)
|
const {setupState, deleteObject, saveObject, isUpdate, editingObjName, applyItemDefaults, loading, editingObj, modelClass} = useModelEditorFunctions<MealPlan>('MealPlan', emit)
|
||||||
|
|
||||||
// object specific data (for selects/display)
|
// object specific data (for selects/display)
|
||||||
|
const tab = ref('shopping')
|
||||||
|
|
||||||
const dateRangeValue = ref([] as Date[])
|
const dateRangeValue = ref([] as Date[])
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const api = new ApiApi()
|
const api = new ApiApi()
|
||||||
|
|
||||||
|
// load meal types and create new object based on default type when initially loading
|
||||||
|
// TODO remove this once moved to user preference from MealType property
|
||||||
|
loading.value = true
|
||||||
api.apiMealTypeList().then(r => {
|
api.apiMealTypeList().then(r => {
|
||||||
|
|
||||||
// TODO remove this once moved to user preference from MealType property
|
|
||||||
let defaultMealType = {} as MealType
|
let defaultMealType = {} as MealType
|
||||||
r.results.forEach(r => {
|
r.results.forEach(r => {
|
||||||
if (r._default) {
|
if (r._default) {
|
||||||
@@ -107,7 +149,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
setupState(props.item, props.itemId, {
|
setupState(props.item, props.itemId, {
|
||||||
newItemFunction: () => {
|
newItemFunction: () => {
|
||||||
console.log('running new Item Function')
|
|
||||||
editingObj.value.fromDate = DateTime.now().toJSDate()
|
editingObj.value.fromDate = DateTime.now().toJSDate()
|
||||||
editingObj.value.toDate = DateTime.now().toJSDate()
|
editingObj.value.toDate = DateTime.now().toJSDate()
|
||||||
editingObj.value.shared = useUserPreferenceStore().userSettings.planShare
|
editingObj.value.shared = useUserPreferenceStore().userSettings.planShare
|
||||||
@@ -119,13 +160,12 @@ onMounted(() => {
|
|||||||
applyItemDefaults(props.itemDefaults)
|
applyItemDefaults(props.itemDefaults)
|
||||||
|
|
||||||
initializeDateRange()
|
initializeDateRange()
|
||||||
console.log(editingObj.value)
|
|
||||||
}, existingItemFunction: () => {
|
}, existingItemFunction: () => {
|
||||||
initializeDateRange()
|
initializeDateRange()
|
||||||
|
useShoppingStore().refreshFromAPI(editingObj.value.id!)
|
||||||
}
|
}
|
||||||
},)
|
},)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1252,6 +1252,7 @@ export interface ApiShoppingListEntryDestroyRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiShoppingListEntryListRequest {
|
export interface ApiShoppingListEntryListRequest {
|
||||||
|
mealplan?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
updatedAfter?: Date;
|
updatedAfter?: Date;
|
||||||
@@ -1285,6 +1286,7 @@ export interface ApiShoppingListRecipeDestroyRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiShoppingListRecipeListRequest {
|
export interface ApiShoppingListRecipeListRequest {
|
||||||
|
mealplan?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
@@ -4023,7 +4025,7 @@ export class ApiApi extends runtime.BaseAPI {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
async apiGetRecipeFileRetrieveRaw(requestParameters: ApiGetRecipeFileRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction) {
|
async apiGetRecipeFileRetrieveRaw(requestParameters: ApiGetRecipeFileRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
|
||||||
if (requestParameters['recipeId'] == null) {
|
if (requestParameters['recipeId'] == null) {
|
||||||
throw new runtime.RequiredError(
|
throw new runtime.RequiredError(
|
||||||
'recipeId',
|
'recipeId',
|
||||||
@@ -4046,13 +4048,13 @@ export class ApiApi extends runtime.BaseAPI {
|
|||||||
query: queryParameters,
|
query: queryParameters,
|
||||||
}, initOverrides);
|
}, initOverrides);
|
||||||
|
|
||||||
return response;
|
return new runtime.VoidApiResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
async apiGetRecipeFileRetrieve(requestParameters: ApiGetRecipeFileRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction){
|
async apiGetRecipeFileRetrieve(requestParameters: ApiGetRecipeFileRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
|
||||||
return await this.apiGetRecipeFileRetrieveRaw(requestParameters, initOverrides);
|
await this.apiGetRecipeFileRetrieveRaw(requestParameters, initOverrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9243,6 +9245,10 @@ export class ApiApi extends runtime.BaseAPI {
|
|||||||
async apiShoppingListEntryListRaw(requestParameters: ApiShoppingListEntryListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedShoppingListEntryList>> {
|
async apiShoppingListEntryListRaw(requestParameters: ApiShoppingListEntryListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedShoppingListEntryList>> {
|
||||||
const queryParameters: any = {};
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
if (requestParameters['mealplan'] != null) {
|
||||||
|
queryParameters['mealplan'] = requestParameters['mealplan'];
|
||||||
|
}
|
||||||
|
|
||||||
if (requestParameters['page'] != null) {
|
if (requestParameters['page'] != null) {
|
||||||
queryParameters['page'] = requestParameters['page'];
|
queryParameters['page'] = requestParameters['page'];
|
||||||
}
|
}
|
||||||
@@ -9532,6 +9538,10 @@ export class ApiApi extends runtime.BaseAPI {
|
|||||||
async apiShoppingListRecipeListRaw(requestParameters: ApiShoppingListRecipeListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedShoppingListRecipeList>> {
|
async apiShoppingListRecipeListRaw(requestParameters: ApiShoppingListRecipeListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PaginatedShoppingListRecipeList>> {
|
||||||
const queryParameters: any = {};
|
const queryParameters: any = {};
|
||||||
|
|
||||||
|
if (requestParameters['mealplan'] != null) {
|
||||||
|
queryParameters['mealplan'] = requestParameters['mealplan'];
|
||||||
|
}
|
||||||
|
|
||||||
if (requestParameters['page'] != null) {
|
if (requestParameters['page'] != null) {
|
||||||
queryParameters['page'] = requestParameters['page'];
|
queryParameters['page'] = requestParameters['page'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export interface MealPlan {
|
|||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
* @memberof MealPlan
|
* @memberof MealPlan
|
||||||
*/
|
*/
|
||||||
addshopping: boolean;
|
addshopping?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,7 +145,6 @@ export function instanceOfMealPlan(value: object): value is MealPlan {
|
|||||||
if (!('recipeName' in value) || value['recipeName'] === undefined) return false;
|
if (!('recipeName' in value) || value['recipeName'] === undefined) return false;
|
||||||
if (!('mealTypeName' in value) || value['mealTypeName'] === undefined) return false;
|
if (!('mealTypeName' in value) || value['mealTypeName'] === undefined) return false;
|
||||||
if (!('shopping' in value) || value['shopping'] === undefined) return false;
|
if (!('shopping' in value) || value['shopping'] === undefined) return false;
|
||||||
if (!('addshopping' in value) || value['addshopping'] === undefined) return false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +172,7 @@ export function MealPlanFromJSONTyped(json: any, ignoreDiscriminator: boolean):
|
|||||||
'recipeName': json['recipe_name'],
|
'recipeName': json['recipe_name'],
|
||||||
'mealTypeName': json['meal_type_name'],
|
'mealTypeName': json['meal_type_name'],
|
||||||
'shopping': json['shopping'],
|
'shopping': json['shopping'],
|
||||||
'addshopping': json['addshopping'],
|
'addshopping': json['addshopping'] == null ? undefined : json['addshopping'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {acceptHMRUpdate, defineStore} from "pinia"
|
import {acceptHMRUpdate, defineStore} from "pinia"
|
||||||
import {ApiApi, Food, Recipe, ShoppingListEntry, ShoppingListEntryBulk, ShoppingListRecipe, Supermarket, SupermarketCategory} from "@/openapi";
|
import {ApiApi, ApiShoppingListEntryListRequest, Food, Recipe, ShoppingListEntry, ShoppingListEntryBulk, ShoppingListRecipe, Supermarket, SupermarketCategory} from "@/openapi";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import {
|
import {
|
||||||
IShoppingExportEntry,
|
IShoppingExportEntry,
|
||||||
@@ -149,6 +149,25 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* very simple list of shopping list entries as IShoppingListFood array filtered by a certain mealplan
|
||||||
|
* @param mealPlanId ID of mealplan
|
||||||
|
*/
|
||||||
|
function getMealPlanEntries(mealPlanId: number) {
|
||||||
|
let items: IShoppingListFood[] = []
|
||||||
|
|
||||||
|
entries.value.forEach(shoppingListEntry => {
|
||||||
|
if (shoppingListEntry.recipeMealplan && shoppingListEntry.recipeMealplan.mealplan == mealPlanId) {
|
||||||
|
items.push({
|
||||||
|
food: shoppingListEntry.food,
|
||||||
|
entries: new Map<number, ShoppingListEntry>().set(shoppingListEntry.id!, shoppingListEntry)
|
||||||
|
} as IShoppingListFood)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if failed items are contained in the sync queue
|
* checks if failed items are contained in the sync queue
|
||||||
*/
|
*/
|
||||||
@@ -163,16 +182,22 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all shopping related data (shopping list entries, supermarkets, supermarket categories and shopping list recipes) from API
|
* Retrieves all shopping related data (shopping list entries, supermarkets, supermarket categories and shopping list recipes) from API
|
||||||
|
* @param mealPlanId optionally filter by mealplan ID and only load entries associated with that
|
||||||
*/
|
*/
|
||||||
function refreshFromAPI() {
|
function refreshFromAPI(mealPlanId?: number) {
|
||||||
if (!currentlyUpdating.value) {
|
if (!currentlyUpdating.value) {
|
||||||
currentlyUpdating.value = true
|
currentlyUpdating.value = true
|
||||||
autoSyncLastTimestamp.value = new Date();
|
autoSyncLastTimestamp.value = new Date();
|
||||||
|
|
||||||
let api = new ApiApi()
|
let api = new ApiApi()
|
||||||
api.apiShoppingListEntryList().then((r) => {
|
let requestParameters = {pageSize: 200} as ApiShoppingListEntryListRequest
|
||||||
|
if (mealPlanId) {
|
||||||
|
requestParameters.mealplan = mealPlanId
|
||||||
|
}
|
||||||
|
|
||||||
|
api.apiShoppingListEntryList(requestParameters).then((r) => {
|
||||||
entries.value = new Map<number, ShoppingListEntry>
|
entries.value = new Map<number, ShoppingListEntry>
|
||||||
// TODO load all pages
|
// TODO properly load pages
|
||||||
r.results.forEach((e) => {
|
r.results.forEach((e) => {
|
||||||
entries.value.set(e.id!, e)
|
entries.value.set(e.id!, e)
|
||||||
})
|
})
|
||||||
@@ -183,7 +208,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
|||||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
api.apiSupermarketList().then(r => {
|
api.apiSupermarketCategoryList().then(r => {
|
||||||
supermarketCategories.value = r.results
|
supermarketCategories.value = r.results
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
|
||||||
@@ -552,7 +577,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
|||||||
setFoodIgnoredState,
|
setFoodIgnoredState,
|
||||||
delayEntries: setEntriesDelayedState,
|
delayEntries: setEntriesDelayedState,
|
||||||
getAssociatedRecipes,
|
getAssociatedRecipes,
|
||||||
|
getMealPlanEntries,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user