mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
Add to Shopping List Dialog
This commit is contained in:
parent
ee68f95d4b
commit
0dbb837dff
1 changed files with 201 additions and 226 deletions
|
@ -51,7 +51,7 @@
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-if="shoppingListIngredientDialog"
|
v-if="shoppingListIngredientDialog"
|
||||||
v-model="dialog"
|
v-model="dialog"
|
||||||
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
:title="selectedShoppingList?.name || $t('recipe.add-to-list')"
|
||||||
:icon="$globals.icons.cartCheck"
|
:icon="$globals.icons.cartCheck"
|
||||||
width="70%"
|
width="70%"
|
||||||
:submit-text="$t('recipe.add-to-list')"
|
:submit-text="$t('recipe.add-to-list')"
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
color="secondary"
|
color="secondary"
|
||||||
density="compact"
|
density="compact"
|
||||||
/>
|
/>
|
||||||
<div :key="ingredientData.ingredient.quantity">
|
<div :key="`${ingredientData.ingredient.quantity || 'no-qty'}-${i}`">
|
||||||
<RecipeIngredientListItem
|
<RecipeIngredientListItem
|
||||||
:ingredient="ingredientData.ingredient"
|
:ingredient="ingredientData.ingredient"
|
||||||
:disable-amount="ingredientData.disableAmount"
|
:disable-amount="ingredientData.disableAmount"
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { toRefs } from "@vueuse/core";
|
import { toRefs } from "@vueuse/core";
|
||||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
|
@ -203,240 +203,215 @@ export interface ShoppingListRecipeIngredientSection {
|
||||||
ingredientSections: ShoppingListIngredientSection[];
|
ingredientSections: ShoppingListIngredientSection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipes?: RecipeWithScale[];
|
||||||
RecipeIngredientListItem,
|
shoppingLists?: ShoppingListSummary[];
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
recipes: undefined,
|
||||||
|
shoppingLists: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const api = useUserApi();
|
||||||
|
const preferences = useShoppingListPreferences();
|
||||||
|
const ready = ref(false);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
shoppingListDialog: true,
|
||||||
|
shoppingListIngredientDialog: false,
|
||||||
|
shoppingListShowAllToggled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllToggled: _shoppingListShowAllToggled } = toRefs(state);
|
||||||
|
|
||||||
|
const userHousehold = computed(() => {
|
||||||
|
return $auth.user.value?.householdSlug || "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const shoppingListChoices = computed(() => {
|
||||||
|
return props.shoppingLists.filter(list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
|
||||||
|
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
||||||
|
|
||||||
|
watchEffect(
|
||||||
|
() => {
|
||||||
|
if (shoppingListChoices.value.length === 1 && !state.shoppingListShowAllToggled) {
|
||||||
|
selectedShoppingList.value = shoppingListChoices.value[0];
|
||||||
|
openShoppingListIngredientDialog(selectedShoppingList.value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ready.value = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
props: {
|
);
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
recipes: {
|
|
||||||
type: Array as () => RecipeWithScale[],
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
shoppingLists: {
|
|
||||||
type: Array as () => ShoppingListSummary[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const api = useUserApi();
|
|
||||||
const preferences = useShoppingListPreferences();
|
|
||||||
const ready = ref(false);
|
|
||||||
|
|
||||||
// v-model support
|
watch(dialog, (val) => {
|
||||||
const dialog = computed({
|
if (!val) {
|
||||||
get: () => {
|
initState();
|
||||||
return props.modelValue;
|
}
|
||||||
},
|
});
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||||
initState();
|
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
|
||||||
},
|
for (const recipe of recipes) {
|
||||||
|
if (!recipe.slug) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipeSectionMap.has(recipe.slug)) {
|
||||||
|
const existingSection = recipeSectionMap.get(recipe.slug);
|
||||||
|
if (existingSection) {
|
||||||
|
existingSection.recipeScale += recipe.scale;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(recipe.id && recipe.name && recipe.recipeIngredient)) {
|
||||||
|
const { data } = await api.recipes.getOne(recipe.slug);
|
||||||
|
if (!data?.recipeIngredient?.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
recipe.id = data.id || "";
|
||||||
|
recipe.name = data.name || "";
|
||||||
|
recipe.recipeIngredient = data.recipeIngredient;
|
||||||
|
}
|
||||||
|
else if (!recipe.recipeIngredient.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shoppingListIngredients: ShoppingListIngredient[] = recipe.recipeIngredient.map((ing) => {
|
||||||
|
const householdsWithFood = (ing.food?.householdsWithIngredientFood || []);
|
||||||
|
return {
|
||||||
|
checked: !householdsWithFood.includes(userHousehold.value),
|
||||||
|
ingredient: ing,
|
||||||
|
disableAmount: recipe.settings?.disableAmount || false,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
let currentTitle = "";
|
||||||
shoppingListDialog: true,
|
const onHandIngs: ShoppingListIngredient[] = [];
|
||||||
shoppingListIngredientDialog: false,
|
const shoppingListIngredientSections = shoppingListIngredients.reduce((sections, ing) => {
|
||||||
shoppingListShowAllToggled: false,
|
if (ing.ingredient.title) {
|
||||||
});
|
currentTitle = ing.ingredient.title;
|
||||||
|
}
|
||||||
|
|
||||||
const userHousehold = computed(() => {
|
// If this is the first item in the section, create a new section
|
||||||
return $auth.user.value?.householdSlug || "";
|
if (sections.length === 0 || currentTitle !== sections[sections.length - 1].sectionName) {
|
||||||
});
|
if (sections.length) {
|
||||||
|
// Add the on-hand ingredients to the previous section
|
||||||
const shoppingListChoices = computed(() => {
|
sections[sections.length - 1].ingredients.push(...onHandIngs);
|
||||||
return props.shoppingLists.filter(list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id);
|
onHandIngs.length = 0;
|
||||||
});
|
|
||||||
|
|
||||||
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
|
|
||||||
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
|
||||||
|
|
||||||
watchEffect(
|
|
||||||
() => {
|
|
||||||
if (shoppingListChoices.value.length === 1 && !state.shoppingListShowAllToggled) {
|
|
||||||
selectedShoppingList.value = shoppingListChoices.value[0];
|
|
||||||
openShoppingListIngredientDialog(selectedShoppingList.value);
|
|
||||||
}
|
}
|
||||||
else {
|
sections.push({
|
||||||
ready.value = true;
|
sectionName: currentTitle,
|
||||||
|
ingredients: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the on-hand ingredients for later
|
||||||
|
const householdsWithFood = (ing.ingredient.food?.householdsWithIngredientFood || []);
|
||||||
|
if (householdsWithFood.includes(userHousehold.value)) {
|
||||||
|
onHandIngs.push(ing);
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the ingredient to previous section
|
||||||
|
sections[sections.length - 1].ingredients.push(ing);
|
||||||
|
return sections;
|
||||||
|
}, [] as ShoppingListIngredientSection[]);
|
||||||
|
|
||||||
|
// Add remaining on-hand ingredients to the previous section
|
||||||
|
shoppingListIngredientSections[shoppingListIngredientSections.length - 1].ingredients.push(...onHandIngs);
|
||||||
|
|
||||||
|
recipeSectionMap.set(recipe.slug, {
|
||||||
|
recipeId: recipe.id,
|
||||||
|
recipeName: recipe.name,
|
||||||
|
recipeScale: recipe.scale,
|
||||||
|
ingredientSections: shoppingListIngredientSections,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeIngredientSections.value = Array.from(recipeSectionMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function initState() {
|
||||||
|
state.shoppingListDialog = true;
|
||||||
|
state.shoppingListIngredientDialog = false;
|
||||||
|
state.shoppingListShowAllToggled = false;
|
||||||
|
recipeIngredientSections.value = [];
|
||||||
|
selectedShoppingList.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initState();
|
||||||
|
|
||||||
|
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||||
|
if (!props.recipes?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedShoppingList.value = list;
|
||||||
|
await consolidateRecipesIntoSections(props.recipes);
|
||||||
|
state.shoppingListDialog = false;
|
||||||
|
state.shoppingListIngredientDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setShowAllToggled() {
|
||||||
|
state.shoppingListShowAllToggled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bulkCheckIngredients(value = true) {
|
||||||
|
recipeIngredientSections.value.forEach((recipeSection) => {
|
||||||
|
recipeSection.ingredientSections.forEach((ingSection) => {
|
||||||
|
ingSection.ingredients.forEach((ing) => {
|
||||||
|
ing.checked = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRecipesToList() {
|
||||||
|
if (!selectedShoppingList.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipeData: ShoppingListAddRecipeParamsBulk[] = [];
|
||||||
|
recipeIngredientSections.value.forEach((section) => {
|
||||||
|
const ingredients: RecipeIngredient[] = [];
|
||||||
|
section.ingredientSections.forEach((ingSection) => {
|
||||||
|
ingSection.ingredients.forEach((ing) => {
|
||||||
|
if (ing.checked) {
|
||||||
|
ingredients.push(ing.ingredient);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ingredients.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeData.push(
|
||||||
|
{
|
||||||
|
recipeId: section.recipeId,
|
||||||
|
recipeIncrementQuantity: section.recipeScale,
|
||||||
|
recipeIngredients: ingredients,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
const { error } = await api.shopping.lists.addRecipes(selectedShoppingList.value.id, recipeData);
|
||||||
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
for (const recipe of recipes) {
|
error ? alert.error(i18n.t("recipe.failed-to-add-recipes-to-list")) : alert.success(i18n.t("recipe.successfully-added-to-list"));
|
||||||
if (!recipe.slug) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipeSectionMap.has(recipe.slug)) {
|
state.shoppingListDialog = false;
|
||||||
recipeSectionMap.get(recipe.slug).recipeScale += recipe.scale;
|
state.shoppingListIngredientDialog = false;
|
||||||
continue;
|
dialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(recipe.id && recipe.name && recipe.recipeIngredient)) {
|
|
||||||
const { data } = await api.recipes.getOne(recipe.slug);
|
|
||||||
if (!data?.recipeIngredient?.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
recipe.id = data.id || "";
|
|
||||||
recipe.name = data.name || "";
|
|
||||||
recipe.recipeIngredient = data.recipeIngredient;
|
|
||||||
}
|
|
||||||
else if (!recipe.recipeIngredient.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shoppingListIngredients: ShoppingListIngredient[] = recipe.recipeIngredient.map((ing) => {
|
|
||||||
const householdsWithFood = (ing.food?.householdsWithIngredientFood || []);
|
|
||||||
return {
|
|
||||||
checked: !householdsWithFood.includes(userHousehold.value),
|
|
||||||
ingredient: ing,
|
|
||||||
disableAmount: recipe.settings?.disableAmount || false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentTitle = "";
|
|
||||||
const onHandIngs: ShoppingListIngredient[] = [];
|
|
||||||
const shoppingListIngredientSections = shoppingListIngredients.reduce((sections, ing) => {
|
|
||||||
if (ing.ingredient.title) {
|
|
||||||
currentTitle = ing.ingredient.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the first item in the section, create a new section
|
|
||||||
if (sections.length === 0 || currentTitle !== sections[sections.length - 1].sectionName) {
|
|
||||||
if (sections.length) {
|
|
||||||
// Add the on-hand ingredients to the previous section
|
|
||||||
sections[sections.length - 1].ingredients.push(...onHandIngs);
|
|
||||||
onHandIngs.length = 0;
|
|
||||||
}
|
|
||||||
sections.push({
|
|
||||||
sectionName: currentTitle,
|
|
||||||
ingredients: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the on-hand ingredients for later
|
|
||||||
const householdsWithFood = (ing.ingredient.food?.householdsWithIngredientFood || []);
|
|
||||||
if (householdsWithFood.includes(userHousehold.value)) {
|
|
||||||
onHandIngs.push(ing);
|
|
||||||
return sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the ingredient to previous section
|
|
||||||
sections[sections.length - 1].ingredients.push(ing);
|
|
||||||
return sections;
|
|
||||||
}, [] as ShoppingListIngredientSection[]);
|
|
||||||
|
|
||||||
// Add remaining on-hand ingredients to the previous section
|
|
||||||
shoppingListIngredientSections[shoppingListIngredientSections.length - 1].ingredients.push(...onHandIngs);
|
|
||||||
|
|
||||||
recipeSectionMap.set(recipe.slug, {
|
|
||||||
recipeId: recipe.id,
|
|
||||||
recipeName: recipe.name,
|
|
||||||
recipeScale: recipe.scale,
|
|
||||||
ingredientSections: shoppingListIngredientSections,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
recipeIngredientSections.value = Array.from(recipeSectionMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
function initState() {
|
|
||||||
state.shoppingListDialog = true;
|
|
||||||
state.shoppingListIngredientDialog = false;
|
|
||||||
state.shoppingListShowAllToggled = false;
|
|
||||||
recipeIngredientSections.value = [];
|
|
||||||
selectedShoppingList.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
initState();
|
|
||||||
|
|
||||||
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
|
||||||
if (!props.recipes?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedShoppingList.value = list;
|
|
||||||
await consolidateRecipesIntoSections(props.recipes);
|
|
||||||
state.shoppingListDialog = false;
|
|
||||||
state.shoppingListIngredientDialog = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setShowAllToggled() {
|
|
||||||
state.shoppingListShowAllToggled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bulkCheckIngredients(value = true) {
|
|
||||||
recipeIngredientSections.value.forEach((recipeSection) => {
|
|
||||||
recipeSection.ingredientSections.forEach((ingSection) => {
|
|
||||||
ingSection.ingredients.forEach((ing) => {
|
|
||||||
ing.checked = value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addRecipesToList() {
|
|
||||||
if (!selectedShoppingList.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipeData: ShoppingListAddRecipeParamsBulk[] = [];
|
|
||||||
recipeIngredientSections.value.forEach((section) => {
|
|
||||||
const ingredients: RecipeIngredient[] = [];
|
|
||||||
section.ingredientSections.forEach((ingSection) => {
|
|
||||||
ingSection.ingredients.forEach((ing) => {
|
|
||||||
if (ing.checked) {
|
|
||||||
ingredients.push(ing.ingredient);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!ingredients.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
recipeData.push(
|
|
||||||
{
|
|
||||||
recipeId: section.recipeId,
|
|
||||||
recipeIncrementQuantity: section.recipeScale,
|
|
||||||
recipeIngredients: ingredients,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { error } = await api.shopping.lists.addRecipes(selectedShoppingList.value.id, recipeData);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
||||||
error ? alert.error(i18n.t("recipe.failed-to-add-recipes-to-list")) : alert.success(i18n.t("recipe.successfully-added-to-list"));
|
|
||||||
|
|
||||||
state.shoppingListDialog = false;
|
|
||||||
state.shoppingListIngredientDialog = false;
|
|
||||||
dialog.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
dialog,
|
|
||||||
preferences,
|
|
||||||
ready,
|
|
||||||
shoppingListChoices,
|
|
||||||
...toRefs(state),
|
|
||||||
addRecipesToList,
|
|
||||||
bulkCheckIngredients,
|
|
||||||
openShoppingListIngredientDialog,
|
|
||||||
setShowAllToggled,
|
|
||||||
recipeIngredientSections,
|
|
||||||
selectedShoppingList,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped lang="css">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue