1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-19 05:09:40 +02:00

feat: add meal plan to shopping list (#2653)

* refactored recipe add to shopping list dialog

* added context menu to meal plan day

* cleaned up presentation

* consolidate repeated recipes

* added alerts

* lint

* lint v2

* fixed undefined recipeRef bug

* lint

* made scale + slug implementation less horrible
This commit is contained in:
Michael Genson 2023-11-21 12:11:10 -06:00 committed by GitHub
parent 23e398e0df
commit 0775072ffa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 523 additions and 186 deletions

View file

@ -69,77 +69,12 @@
></v-select>
</v-card-text>
</BaseDialog>
<BaseDialog v-model="shoppingListDialog" :title="$t('recipe.add-to-list')" :icon="$globals.icons.cartCheck">
<v-card-text>
<v-card
v-for="list in shoppingLists"
:key="list.id"
hover
class="my-2 left-border"
@click="openShoppingListIngredientDialog(list)"
>
<v-card-title class="py-2">
{{ list.name }}
</v-card-title>
</v-card>
</v-card-text>
</BaseDialog>
<BaseDialog
v-model="shoppingListIngredientDialog"
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
:icon="$globals.icons.cartCheck"
width="70%"
:submit-text="$tc('recipe.add-to-list')"
@submit="addRecipeToList()"
>
<v-card
elevation="0"
height="fit-content"
max-height="60vh"
width="100%"
:class="$vuetify.breakpoint.smAndDown ? '' : 'ingredient-grid'"
:style="$vuetify.breakpoint.smAndDown ? '' : { gridTemplateRows: `repeat(${Math.ceil(recipeIngredients.length / 2)}, min-content)` }"
style="overflow-y: auto"
>
<v-list-item
v-for="(ingredientData, i) in recipeIngredients"
:key="'ingredient' + i"
dense
@click="recipeIngredients[i].checked = !recipeIngredients[i].checked"
>
<v-checkbox
hide-details
:input-value="ingredientData.checked"
class="pt-0 my-auto py-auto"
color="secondary"
/>
<v-list-item-content :key="ingredientData.ingredient.quantity">
<RecipeIngredientListItem
:ingredient="ingredientData.ingredient"
:disable-amount="ingredientData.disableAmount"
:scale="recipeScale" />
</v-list-item-content>
</v-list-item>
</v-card>
<div class="d-flex justify-end mb-4 mt-2">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.checkboxBlankOutline,
text: $tc('shopping-list.uncheck-all-items'),
event: 'uncheck',
},
{
icon: $globals.icons.checkboxOutline,
text: $tc('shopping-list.check-all-items'),
event: 'check',
},
]"
@uncheck="bulkCheckIngredients(false)"
@check="bulkCheckIngredients(true)"
/>
</div>
</BaseDialog>
<RecipeDialogAddToShoppingList
v-if="shoppingLists && recipeRefWithScale"
v-model="shoppingListDialog"
:recipes="[recipeRefWithScale]"
:shopping-lists="shoppingLists"
/>
<v-menu
offset-y
left
@ -171,14 +106,14 @@
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext, useRoute, useRouter, ref } from "@nuxtjs/composition-api";
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
import RecipeDialogShare from "./RecipeDialogShare.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast";
import { usePlanTypeOptions } from "~/composables/use-group-mealplan";
import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
import { Recipe } from "~/lib/api/types/recipe";
import { ShoppingListSummary } from "~/lib/api/types/group";
import { PlanEntryType } from "~/lib/api/types/meal-plan";
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
@ -204,9 +139,9 @@ export interface ContextMenuItem {
export default defineComponent({
components: {
RecipeDialogAddToShoppingList,
RecipeDialogPrintPreferences,
RecipeDialogShare,
RecipeIngredientListItem
},
props: {
useItems: {
@ -279,7 +214,6 @@ export default defineComponent({
recipeDeleteDialog: false,
mealplannerDialog: false,
shoppingListDialog: false,
shoppingListIngredientDialog: false,
recipeDuplicateDialog: false,
recipeName: props.name,
loading: false,
@ -374,7 +308,7 @@ export default defineComponent({
}
}
// Add leading and Apppending Items
// Add leading and Appending Items
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
const icon = props.menuIcon || $globals.icons.dotsVertical;
@ -383,9 +317,8 @@ export default defineComponent({
// Context Menu Event Handler
const shoppingLists = ref<ShoppingListSummary[]>();
const selectedShoppingList = ref<ShoppingListSummary>();
const recipeRef = ref<Recipe>(props.recipe);
const recipeIngredients = ref<{ checked: boolean; ingredient: RecipeIngredient, disableAmount: boolean }[]>([]);
const recipeRefWithScale = computed(() => recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined);
async function getShoppingLists() {
const { data } = await api.shopping.lists.getAll();
@ -401,61 +334,6 @@ export default defineComponent({
}
}
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
selectedShoppingList.value = list;
if (!recipeRef.value) {
await refreshRecipe();
}
if (recipeRef.value?.recipeIngredient) {
recipeIngredients.value = recipeRef.value.recipeIngredient.map((ingredient) => {
return {
checked: true,
ingredient,
disableAmount: recipeRef.value.settings?.disableAmount || false
};
});
}
state.shoppingListDialog = false;
state.shoppingListIngredientDialog = true;
}
function bulkCheckIngredients(value = true) {
recipeIngredients.value.forEach((data) => {
data.checked = value;
});
}
async function addRecipeToList() {
if (!selectedShoppingList.value) {
return;
}
const ingredients: RecipeIngredient[] = [];
recipeIngredients.value.forEach((data) => {
if (data.checked) {
ingredients.push(data.ingredient);
}
});
if (!ingredients.length) {
return;
}
const { data } = await api.shopping.lists.addRecipe(
selectedShoppingList.value.id,
props.recipeId,
props.recipeScale,
ingredients
);
if (data) {
alert.success(i18n.t("recipe.recipe-added-to-list") as string);
state.shoppingListDialog = false;
state.shoppingListIngredientDialog = false;
}
}
const router = useRouter();
async function deleteRecipe() {
@ -516,10 +394,12 @@ export default defineComponent({
state.printPreferencesDialog = true;
},
shoppingList: () => {
getShoppingLists();
const promises: Promise<void>[] = [getShoppingLists()];
if (!recipeRef.value) {
promises.push(refreshRecipe());
}
state.shoppingListDialog = true;
state.shoppingListIngredientDialog = false;
Promise.allSettled(promises).then(() => { state.shoppingListDialog = true });
},
share: () => {
state.shareDialog = true;
@ -544,28 +424,15 @@ export default defineComponent({
return {
...toRefs(state),
recipeRef,
recipeRefWithScale,
shoppingLists,
selectedShoppingList,
openShoppingListIngredientDialog,
addRecipeToList,
bulkCheckIngredients,
duplicateRecipe,
contextMenuEventHandler,
deleteRecipe,
addRecipeToPlan,
icon,
planTypeOptions,
recipeIngredients,
};
},
});
</script>
<style scoped lang="css">
.ingredient-grid {
display: grid;
grid-auto-flow: column;
grid-template-columns: 1fr 1fr;
grid-gap: 0.5rem;
}
</style>