2024-01-13 02:00:15 +00:00
|
|
|
<template>
|
2025-10-18 02:08:22 +00:00
|
|
|
<div class="container">
|
|
|
|
|
<div class="fields">
|
2025-10-18 03:16:34 +00:00
|
|
|
<date-picker
|
|
|
|
|
:date="meal.suggested_date"
|
|
|
|
|
@date-selected="selectDate"
|
|
|
|
|
/>
|
2025-10-18 02:08:22 +00:00
|
|
|
<div class="persons-list">
|
|
|
|
|
Cooked by
|
|
|
|
|
<person-list
|
|
|
|
|
:people="meal.chefs"
|
|
|
|
|
@remove-person="(p) => removePerson('chefs', p)"
|
|
|
|
|
@add-person="(p) => addPerson('chefs', p)"
|
|
|
|
|
/>
|
|
|
|
|
for
|
|
|
|
|
<person-list
|
|
|
|
|
:people="meal.consumers"
|
|
|
|
|
@remove-person="(p) => removePerson('consumers', p)"
|
|
|
|
|
@add-person="(p) => addPerson('consumers', p)"
|
|
|
|
|
/>, with
|
|
|
|
|
<person-list
|
|
|
|
|
:people="meal.cleanup"
|
|
|
|
|
@remove-person="(p) => removePerson('cleanup', p)"
|
|
|
|
|
@add-person="(p) => addPerson('cleanup', p)"
|
|
|
|
|
/>
|
|
|
|
|
on cleanup.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="recipes">
|
|
|
|
|
<h2>Recipes</h2>
|
|
|
|
|
<ul v-if="meal.recipes && meal.recipes.length">
|
2025-10-18 03:16:34 +00:00
|
|
|
<li
|
|
|
|
|
v-for="mealRecipe in meal.recipes"
|
|
|
|
|
:key="mealRecipe.recipe.id"
|
|
|
|
|
>
|
2025-10-18 02:08:22 +00:00
|
|
|
<div class="saved-recipe">
|
|
|
|
|
<p class="recipe-card">
|
|
|
|
|
<recipe-card :recipe="mealRecipe.recipe" />
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<p class="servings">
|
2025-10-18 03:16:34 +00:00
|
|
|
<input
|
|
|
|
|
v-model="mealRecipe.servings"
|
|
|
|
|
type="number"
|
|
|
|
|
min="1"
|
|
|
|
|
>
|
2025-10-18 02:08:22 +00:00
|
|
|
<small><em>servings</em></small>
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
class="show-ingredient-checkbox"
|
|
|
|
|
:checked="showIngredient(mealRecipe)"
|
2025-10-18 03:16:34 +00:00
|
|
|
>
|
2025-10-18 02:08:22 +00:00
|
|
|
<label
|
|
|
|
|
for="show-ingredients"
|
|
|
|
|
@click="showIngredient(mealRecipe, !showIngredient(mealRecipe))"
|
|
|
|
|
>
|
2025-10-18 03:16:34 +00:00
|
|
|
<img
|
|
|
|
|
class="icon"
|
|
|
|
|
:src="require('@/assets/show-ingredients.svg')"
|
|
|
|
|
>
|
2025-10-18 02:08:22 +00:00
|
|
|
</label>
|
2025-10-18 03:16:34 +00:00
|
|
|
<button
|
|
|
|
|
class="icon-button"
|
|
|
|
|
@click="removeRecipe(mealRecipe)"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
class="icon"
|
|
|
|
|
:src="require('@/assets/trash.svg')"
|
|
|
|
|
>
|
2025-10-18 02:08:22 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="showIngredient(mealRecipe)">
|
|
|
|
|
<ul>
|
|
|
|
|
<li
|
|
|
|
|
v-for="ingredient in scaleIngredients(mealRecipe)"
|
|
|
|
|
:key="ingredient.id"
|
|
|
|
|
class="saved-ingredient"
|
|
|
|
|
>
|
|
|
|
|
<CompactParsedIngredient :ingredient="ingredient" />
|
|
|
|
|
</li>
|
2024-01-14 06:20:21 +00:00
|
|
|
</ul>
|
2025-10-18 02:08:22 +00:00
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<div v-else>
|
|
|
|
|
<p>Add some recipes using the search box</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="fields">
|
|
|
|
|
<recipe-search-box @select-recipe="selectRecipe" />
|
|
|
|
|
</div>
|
2024-01-13 23:09:15 +00:00
|
|
|
</div>
|
2025-10-18 02:08:22 +00:00
|
|
|
<div class="ingredients">
|
|
|
|
|
<h2>Sides & Additional Ingredients</h2>
|
|
|
|
|
<editable-ingredients-panel
|
|
|
|
|
:ingredients="meal.extra_ingredients"
|
|
|
|
|
@on-add="addIngredient"
|
|
|
|
|
@on-delete="deleteIngredient"
|
|
|
|
|
@on-update-ingredient="updateIngredient"
|
|
|
|
|
@on-editing="onEditAdditionalIngredients"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-18 03:16:34 +00:00
|
|
|
<button @click="onSaveMeal">
|
|
|
|
|
Save
|
|
|
|
|
</button>
|
2025-10-18 02:08:22 +00:00
|
|
|
<p v-if="meal.purchase_date">
|
|
|
|
|
<em>Purchased {{ ago(meal.purchase_date) }}</em>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2024-01-13 02:00:15 +00:00
|
|
|
</template>
|
|
|
|
|
|
2025-10-18 01:54:56 +00:00
|
|
|
<script setup>
|
|
|
|
|
import { reactive, onBeforeMount } from 'vue'
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
import { getMeal, saveMeal } from '@/composables/useMeals'
|
|
|
|
|
import { getRecipe } from '@/api/recipes'
|
|
|
|
|
import { currentUser } from '@/api/auth'
|
2025-10-18 03:12:27 +00:00
|
|
|
import { useAlert } from '@/composables/useAlert'
|
2024-09-21 22:02:21 +00:00
|
|
|
|
2025-10-18 02:08:22 +00:00
|
|
|
import { ago } from '@/dateformats.js'
|
2024-10-15 08:40:06 +00:00
|
|
|
|
2025-10-18 02:08:22 +00:00
|
|
|
import RecipeSearchBox from '@/components/recipes/RecipeSearchBox.vue'
|
|
|
|
|
import RecipeCard from '@/components/recipes/RecipeCard.vue'
|
|
|
|
|
import DatePicker from './DatePicker.vue'
|
|
|
|
|
import EditableIngredientsPanel from '../ingredients/EditableIngredientsPanel.vue'
|
|
|
|
|
import PersonList from './PersonList.vue'
|
|
|
|
|
import CompactParsedIngredient from '../ingredients/CompactParsedIngredient.vue'
|
2024-01-13 23:09:15 +00:00
|
|
|
|
2024-05-19 12:40:32 +00:00
|
|
|
function addPersonIfNotExists(list, person) {
|
2025-10-18 02:08:22 +00:00
|
|
|
if (!list.find((p) => p.id === person.id)) {
|
|
|
|
|
list.push(person)
|
|
|
|
|
}
|
2024-05-19 12:40:32 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-18 01:54:56 +00:00
|
|
|
const route = useRoute()
|
|
|
|
|
const router = useRouter()
|
2025-10-18 03:12:27 +00:00
|
|
|
const { show: showAlert } = useAlert()
|
2025-10-18 01:54:56 +00:00
|
|
|
|
|
|
|
|
const showIngredients = reactive({})
|
|
|
|
|
const meal = reactive({
|
2025-10-18 02:08:22 +00:00
|
|
|
id: -1,
|
|
|
|
|
suggested_date: new Date(),
|
|
|
|
|
recipes: [],
|
|
|
|
|
extra_ingredients: [],
|
|
|
|
|
chefs: [],
|
|
|
|
|
consumers: [],
|
|
|
|
|
cleanup: [],
|
2025-10-18 01:54:56 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeMount(async () => {
|
2025-10-18 02:08:22 +00:00
|
|
|
const idParam = route.params.id
|
|
|
|
|
const id = typeof idParam === 'string' ? parseInt(idParam) : idParam
|
|
|
|
|
if (id >= 0) {
|
|
|
|
|
const loaded = await getMeal(id)
|
|
|
|
|
Object.assign(meal, loaded)
|
|
|
|
|
} else {
|
|
|
|
|
const self = await currentUser()
|
|
|
|
|
Object.assign(meal, { chefs: [self], consumers: [self], cleanup: [self] })
|
|
|
|
|
}
|
2025-10-18 01:54:56 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function selectDate(date) {
|
2025-10-18 02:08:22 +00:00
|
|
|
meal.suggested_date = date
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeRecipe(mealRecipe) {
|
2025-10-18 02:08:22 +00:00
|
|
|
if (
|
|
|
|
|
confirm(
|
|
|
|
|
`Are you sure you want to remove ${mealRecipe.servings} servings of ${mealRecipe.recipe.name} from this meal?`
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
meal.recipes = meal.recipes.filter((r) => r != mealRecipe)
|
|
|
|
|
}
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addIngredient() {
|
2025-10-18 02:08:22 +00:00
|
|
|
meal.extra_ingredients = [{ line: '', product: null }, ...meal.extra_ingredients]
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteIngredient(ingredient) {
|
2025-10-18 02:08:22 +00:00
|
|
|
meal.extra_ingredients = meal.extra_ingredients.filter((i) => i != ingredient)
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateIngredient(ingredient, newIngredient) {
|
2025-10-18 02:08:22 +00:00
|
|
|
meal.extra_ingredients = meal.extra_ingredients.map((i) => (i == ingredient ? newIngredient : i))
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removePerson(list, person) {
|
2025-10-18 02:08:22 +00:00
|
|
|
meal[list] = meal[list].filter((p) => p.id !== person.id)
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addPerson(list, person) {
|
2025-10-18 02:08:22 +00:00
|
|
|
addPersonIfNotExists(meal[list], person)
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function selectRecipe(recipe) {
|
2025-10-18 02:08:22 +00:00
|
|
|
// Refetch to get additional details
|
|
|
|
|
recipe = await getRecipe(recipe.id)
|
2025-10-18 01:54:56 +00:00
|
|
|
|
2025-10-18 02:08:22 +00:00
|
|
|
if (recipe.created_by) {
|
|
|
|
|
addPersonIfNotExists(meal.chefs, recipe.created_by)
|
|
|
|
|
addPersonIfNotExists(meal.consumers, recipe.created_by)
|
2025-10-18 01:54:56 +00:00
|
|
|
|
2025-10-18 02:08:22 +00:00
|
|
|
if (meal.cleanup.length === 0) {
|
|
|
|
|
addPersonIfNotExists(meal.cleanup, recipe.created_by)
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
2025-10-18 02:08:22 +00:00
|
|
|
}
|
2025-10-18 01:54:56 +00:00
|
|
|
|
2025-10-18 02:08:22 +00:00
|
|
|
meal.recipes.push({ recipe, recipe_id: recipe.id, meal_id: meal.id, servings: recipe.serves })
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onEditAdditionalIngredients(editing) {
|
2025-10-18 02:08:22 +00:00
|
|
|
if (editing && meal.extra_ingredients.length === 0) {
|
|
|
|
|
addIngredient()
|
|
|
|
|
} else {
|
|
|
|
|
meal.extra_ingredients = meal.extra_ingredients.filter((i) => i.line)
|
|
|
|
|
}
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showIngredient(mealRecipe, value) {
|
2025-10-18 02:08:22 +00:00
|
|
|
const index = meal.recipes.indexOf(mealRecipe)
|
|
|
|
|
const key = `${mealRecipe.recipe.id}-${index}`
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
return showIngredients[key]
|
|
|
|
|
}
|
|
|
|
|
return (showIngredients[key] = value)
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scaleIngredients(mealRecipe) {
|
2025-10-18 02:08:22 +00:00
|
|
|
return mealRecipe.recipe.ingredients.map((i) => {
|
|
|
|
|
return {
|
|
|
|
|
...i,
|
|
|
|
|
quantity: (i.quantity * mealRecipe.servings) / mealRecipe.recipe.serves,
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-10-18 01:54:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onSaveMeal() {
|
2025-10-18 02:08:22 +00:00
|
|
|
const saved = await saveMeal(meal)
|
|
|
|
|
if (saved?.id >= 0) {
|
|
|
|
|
Object.assign(meal, saved)
|
|
|
|
|
router.push(`/meals/${saved.id}`)
|
2025-10-18 03:12:27 +00:00
|
|
|
showAlert({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' })
|
2025-10-18 02:08:22 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-18 03:12:27 +00:00
|
|
|
showAlert({
|
2025-10-18 02:08:22 +00:00
|
|
|
heading: 'Error saving meal',
|
|
|
|
|
message: 'An error occurred while saving the meal',
|
|
|
|
|
type: 'error',
|
|
|
|
|
})
|
2024-01-13 02:00:15 +00:00
|
|
|
}
|
|
|
|
|
</script>
|
2024-01-14 01:46:45 +00:00
|
|
|
|
|
|
|
|
<style scoped>
|
2024-05-11 01:27:26 +00:00
|
|
|
img.icon {
|
2025-10-18 02:08:22 +00:00
|
|
|
width: 2em;
|
|
|
|
|
height: 2em;
|
2024-05-11 01:27:26 +00:00
|
|
|
}
|
|
|
|
|
|
2024-05-04 04:22:37 +00:00
|
|
|
.persons-list {
|
2025-10-18 02:08:22 +00:00
|
|
|
text-align: left;
|
|
|
|
|
padding: 1ex 2em;
|
2024-05-04 04:22:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.persons-list p {
|
2025-10-18 02:08:22 +00:00
|
|
|
margin: 0;
|
|
|
|
|
padding-bottom: 1em;
|
2024-05-04 04:22:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.person-list li {
|
2025-10-18 02:08:22 +00:00
|
|
|
display: inline-block;
|
|
|
|
|
padding-right: 1em;
|
2024-05-04 04:22:37 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-14 01:46:45 +00:00
|
|
|
.container {
|
2025-10-18 02:08:22 +00:00
|
|
|
margin: 1em auto;
|
2024-01-14 01:46:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ul {
|
2025-10-18 02:08:22 +00:00
|
|
|
list-style-type: none;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
2024-01-14 01:46:45 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-17 13:34:42 +00:00
|
|
|
li {
|
2025-10-18 02:08:22 +00:00
|
|
|
padding-bottom: 0.5vh;
|
2024-01-17 13:34:42 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-14 06:20:21 +00:00
|
|
|
.saved-ingredient {
|
2025-10-18 02:08:22 +00:00
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
2024-01-14 06:20:21 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-14 01:46:45 +00:00
|
|
|
.saved-recipe {
|
2025-10-18 02:08:22 +00:00
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
2024-01-14 01:46:45 +00:00
|
|
|
}
|
|
|
|
|
|
2024-01-14 06:20:21 +00:00
|
|
|
.saved-ingredient p {
|
2025-10-18 02:08:22 +00:00
|
|
|
flex: 1;
|
|
|
|
|
margin: 0;
|
|
|
|
|
margin-right: 1em;
|
2024-01-14 01:46:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.saved-recipe button:hover {
|
2025-10-18 02:08:22 +00:00
|
|
|
background: #eee;
|
2024-01-14 01:46:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.saved-recipe .recipe-card {
|
2025-10-18 02:08:22 +00:00
|
|
|
margin: 0;
|
2024-01-14 01:46:45 +00:00
|
|
|
}
|
|
|
|
|
|
2024-09-28 04:57:32 +00:00
|
|
|
.recipe-card {
|
2025-10-18 02:08:22 +00:00
|
|
|
flex: 1;
|
2024-09-28 04:57:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.servings {
|
2025-10-18 02:08:22 +00:00
|
|
|
display: inline-block;
|
|
|
|
|
margin-right: 1em;
|
2024-09-28 04:57:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.servings input {
|
2025-10-18 02:08:22 +00:00
|
|
|
width: 3em;
|
|
|
|
|
border: none;
|
|
|
|
|
border-bottom: 1px solid #000;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: large;
|
|
|
|
|
font-style: italic;
|
2024-09-28 04:57:32 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-15 08:15:35 +00:00
|
|
|
.icon-button {
|
2025-10-18 02:08:22 +00:00
|
|
|
background: none;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-radius: 1em;
|
|
|
|
|
padding: 0.5em;
|
2024-09-28 04:57:32 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-15 08:15:35 +00:00
|
|
|
.icon-button:hover {
|
2025-10-18 02:08:22 +00:00
|
|
|
/* Invert the colors of the trash icon */
|
|
|
|
|
filter: invert(1);
|
2024-09-28 04:57:32 +00:00
|
|
|
}
|
|
|
|
|
|
2024-10-15 08:15:35 +00:00
|
|
|
.show-ingredient-checkbox {
|
2025-10-18 02:08:22 +00:00
|
|
|
display: none;
|
2024-10-15 08:15:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.show-ingredient-checkbox + label {
|
2025-10-18 02:08:22 +00:00
|
|
|
cursor: pointer;
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
padding: 0.5em;
|
|
|
|
|
border-radius: 1em;
|
2024-10-15 08:15:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.show-ingredient-checkbox + label:hover {
|
2025-10-18 02:08:22 +00:00
|
|
|
background-color: #eee;
|
2024-10-15 08:15:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.show-ingredient-checkbox:checked + label {
|
2025-10-18 02:08:22 +00:00
|
|
|
filter: invert(1);
|
2024-10-15 08:15:35 +00:00
|
|
|
}
|
2025-10-18 02:08:22 +00:00
|
|
|
</style>
|