This commit is contained in:
jableader 2025-10-18 12:54:56 +11:00
parent a6904524db
commit 8797a5489c
4 changed files with 165 additions and 139 deletions

View file

@ -41,6 +41,7 @@ Outcome: Components get smaller and easier to read; business logic is reusable.
- [x] Add Prettier config and .editorconfig; wire Prettier with ESLint
- [ ] Upgrade ESLint (if/when convenient) and align with Vue 3 rules
- [ ] Ensure Volar is used (dev environment) for Vue 3 type intelligence
- Optional next: add lint-staged + husky for `pre-commit` formatting
Outcome: Stable formatting and consistent linting across contributors.

View file

@ -52,16 +52,17 @@
<editable-ingredients-panel :ingredients="meal.extra_ingredients" @on-add="addIngredient" @on-delete="deleteIngredient" @on-update-ingredient="updateIngredient" @on-editing="onEditAdditionalIngredients" />
</div>
<button @click="saveMeal">Save</button>
<button @click="onSaveMeal">Save</button>
<p v-if="meal.purchase_date"><em>Purchased {{ ago(meal.purchase_date) }}</em></p>
</div>
</template>
<script>
import { getMeal, saveMeal } from '@/api/meals';
import { getRecipe } from '@/api/recipes';
import { currentUser } from '@/api/auth';
<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'
import alert from '@/alert.js';
import { ago } from '@/dateformats.js';
@ -79,110 +80,114 @@ function addPersonIfNotExists(list, person) {
}
}
export default {
props: ['id'],
components: { RecipeSearchBox, DatePicker, RecipeCard, EditableIngredientsPanel, PersonList, CompactParsedIngredient },
data() {
return {
showIngredients: {},
meal: {
id: -1,
suggested_date: new Date(),
recipes: [],
extra_ingredients: [],
chefs: [],
consumers: [],
cleanup: []
}
};
},
async beforeMount() {
if (this.id >= 0) {
this.meal = await getMeal(this.id);
}
else {
const self = await currentUser();
this.meal = {...this.meal, chefs: [self], consumers: [self], cleanup: [self], };
}
},
methods: {
ago,
async selectRecipe(recipe) {
// Refetch to get additional details
recipe = await getRecipe(recipe.id);
const route = useRoute()
const router = useRouter()
if (recipe.created_by) {
addPersonIfNotExists(this.meal.chefs, recipe.created_by);
addPersonIfNotExists(this.meal.consumers, recipe.created_by);
const showIngredients = reactive({})
const meal = reactive({
id: -1,
suggested_date: new Date(),
recipes: [],
extra_ingredients: [],
chefs: [],
consumers: [],
cleanup: [],
})
if (this.meal.cleanup.length === 0) {
addPersonIfNotExists(this.meal.cleanup, recipe.created_by);
}
}
this.meal.recipes.push({ recipe, recipe_id: recipe.id, meal_id: this.meal.id, servings: recipe.serves });
},
selectDate(date) {
this.meal.suggested_date = date;
},
removeRecipe(mealRecipe) {
if (confirm(`Are you sure you want to remove ${mealRecipe.servings} servings of ${mealRecipe.recipe.name} from this meal?`)) {
this.meal.recipes = this.meal.recipes.filter(r => r != mealRecipe);
}
},
addIngredient() {
this.meal.extra_ingredients = [{ line: '', product: null }, ...this.meal.extra_ingredients];
},
deleteIngredient(ingredient) {
this.meal.extra_ingredients = this.meal.extra_ingredients.filter(i => i != ingredient);
},
updateIngredient(ingredient, newIngredient) {
this.meal.extra_ingredients = this.meal.extra_ingredients.map(i => i == ingredient ? newIngredient : i);
},
removePerson(list, person) {
this.meal[list] = this.meal[list].filter(p => p.id !== person.id);
},
addPerson(list, person) {
addPersonIfNotExists(this.meal[list], person);
},
async saveMeal() {
const meal = await saveMeal(this.meal);
if (meal?.id >= 0) {
this.meal = meal;
this.$router.push(`/meals/${meal.id}`);
alert.show({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' });
return;
}
alert.show({ heading: 'Error saving meal', message: 'An error occurred while saving the meal', type: 'error' });
},
onEditAdditionalIngredients(editing) {
if (editing && this.meal.extra_ingredients.length === 0) {
this.addIngredient();
}
else {
this.meal.extra_ingredients = this.meal.extra_ingredients.filter(i => i.line);
}
},
showIngredient(mealRecipe, value) {
const index = this.meal.recipes.indexOf(mealRecipe);
const key = `${mealRecipe.recipe.id}-${index}`;
if (value === undefined) {
return this.showIngredients[key];
}
return this.showIngredients[key] = value;
},
scaleIngredients(mealRecipe) {
return mealRecipe.recipe.ingredients.map(i => {
return {
...i,
quantity: i.quantity * mealRecipe.servings / mealRecipe.recipe.serves
};
});
},
onBeforeMount(async () => {
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] })
}
})
function selectDate(date) {
meal.suggested_date = date
}
function removeRecipe(mealRecipe) {
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)
}
}
function addIngredient() {
meal.extra_ingredients = [{ line: '', product: null }, ...meal.extra_ingredients]
}
function deleteIngredient(ingredient) {
meal.extra_ingredients = meal.extra_ingredients.filter((i) => i != ingredient)
}
function updateIngredient(ingredient, newIngredient) {
meal.extra_ingredients = meal.extra_ingredients.map((i) => (i == ingredient ? newIngredient : i))
}
function removePerson(list, person) {
meal[list] = meal[list].filter((p) => p.id !== person.id)
}
function addPerson(list, person) {
addPersonIfNotExists(meal[list], person)
}
async function selectRecipe(recipe) {
// Refetch to get additional details
recipe = await getRecipe(recipe.id)
if (recipe.created_by) {
addPersonIfNotExists(meal.chefs, recipe.created_by)
addPersonIfNotExists(meal.consumers, recipe.created_by)
if (meal.cleanup.length === 0) {
addPersonIfNotExists(meal.cleanup, recipe.created_by)
}
}
meal.recipes.push({ recipe, recipe_id: recipe.id, meal_id: meal.id, servings: recipe.serves })
}
async function onEditAdditionalIngredients(editing) {
if (editing && meal.extra_ingredients.length === 0) {
addIngredient()
} else {
meal.extra_ingredients = meal.extra_ingredients.filter((i) => i.line)
}
}
function showIngredient(mealRecipe, value) {
const index = meal.recipes.indexOf(mealRecipe)
const key = `${mealRecipe.recipe.id}-${index}`
if (value === undefined) {
return showIngredients[key]
}
return (showIngredients[key] = value)
}
function scaleIngredients(mealRecipe) {
return mealRecipe.recipe.ingredients.map((i) => {
return {
...i,
quantity: (i.quantity * mealRecipe.servings) / mealRecipe.recipe.serves,
}
})
}
async function onSaveMeal() {
const saved = await saveMeal(meal)
if (saved?.id >= 0) {
Object.assign(meal, saved)
router.push(`/meals/${saved.id}`)
alert.show({ heading: 'Meal saved', message: 'Meal saved successfully', type: 'success' })
return
}
alert.show({ heading: 'Error saving meal', message: 'An error occurred while saving the meal', type: 'error' })
}
</script>

View file

@ -78,41 +78,36 @@ ul.actions {
</style>
<script>
<script setup>
import { ref, onBeforeMount } from 'vue'
import ActionItem from '@/components/ActionItem.vue'
import MealCard from '@/components/meals/MealCard.vue'
import { getUpcomingMeals, markMealConsumed, deleteMeal } from '@/api/meals'
import { getUpcomingMeals, markMealConsumed, deleteMeal } from '@/composables/useMeals'
export default {
name: 'MealPlanPage',
components: { MealCard, ActionItem },
data() {
const from = new Date();
from.setTime(0);
const to = new Date();
to.setDate(to.getDate() + 7);
return {
from, to,
meals: [],
selectedMeal: null
}
},
async beforeMount() {
const meals = await getUpcomingMeals(this.from, this.to)
this.meals = meals
},
methods: {
async deleteSelectedMeal() {
await deleteMeal(this.selectedMeal.id)
this.meals = this.meals.filter(m => m.id !== this.selectedMeal.id)
this.selectedMeal = null
},
async markConsumed() {
await markMealConsumed(this.selectedMeal.id)
this.meals = this.meals.filter(m => m.id !== this.selectedMeal.id)
}
}
const from = new Date()
from.setTime(0)
const to = new Date()
to.setDate(to.getDate() + 7)
const meals = ref([])
const selectedMeal = ref(null)
onBeforeMount(async () => {
meals.value = await getUpcomingMeals(from, to)
})
async function deleteSelectedMeal() {
if (!selectedMeal.value) return
await deleteMeal(selectedMeal.value.id)
meals.value = meals.value.filter((m) => m.id !== selectedMeal.value.id)
selectedMeal.value = null
}
async function markConsumed() {
if (!selectedMeal.value) return
await markMealConsumed(selectedMeal.value.id)
meals.value = meals.value.filter((m) => m.id !== selectedMeal.value.id)
}
</script>

View file

@ -0,0 +1,25 @@
import * as api from '@/api/meals'
export async function getUpcomingMeals(from, to) {
return api.getUpcomingMeals(from, to)
}
export async function getMeal(id) {
return api.getMeal(id)
}
export async function saveMeal(meal) {
return api.saveMeal(meal)
}
export async function markMealConsumed(mealId) {
return api.markMealConsumed(mealId)
}
export async function deleteMeal(mealId) {
return api.deleteMeal(mealId)
}
export function useMeals() {
return { getUpcomingMeals, getMeal, saveMeal, markMealConsumed, deleteMeal }
}