This commit is contained in:
jableader 2025-10-18 13:01:18 +11:00
parent 8797a5489c
commit 2cc87e1891
5 changed files with 159 additions and 175 deletions

View file

@ -32,8 +32,10 @@ Outcome: Feature modules call cohesive services; logic for mapping/normalization
### Phase 3 — Composables (UI-Facing Logic)
- [x] Add `src/composables/useAuth.js` (user ref, ensureAuth)
- [ ] Add `src/composables/useMeals.js` (fetch and mutate meals)
- [ ] Refactor pages to use composables and `<script setup>` where appropriate
- [x] Add `src/composables/useMeals.js` (fetch and mutate meals)
- [x] Refactor pages to use composables and `<script setup>` where appropriate
- Converted: `MealPlanPage.vue`, `EditMealPage.vue`, `CurrentShoppingListPage.vue`, `MyShoppingPage.vue`, `PurchasedShoppingListPage.vue`
- Added: `src/composables/useShopping.js`; adopted by shopping pages
Outcome: Components get smaller and easier to read; business logic is reusable.
@ -66,3 +68,5 @@ Outcome: Confidence in refactors and easier onboarding.
- Added `api/auth.js` and `api/persons.js`; router uses auth API
- Added `composables/useAuth.js` and used in `MyShoppingPage.vue`
- Added Prettier and EditorConfig
- Added `composables/useMeals.js`; migrated meal pages to composable and `<script setup>`
- Added `composables/useShopping.js`; migrated shopping pages to composable and `<script setup>`

View file

@ -133,119 +133,93 @@ button img {
</style>
<script>
<script setup>
import { ref, computed, onBeforeMount } from 'vue'
import { useRouter } from 'vue-router'
import alert from '@/alert.js'
import { getCurrentShoppingList, purchaseShoppingList, requestMeal, unrequestMeal } from '@/api/shopping'
import { useShopping } from '@/composables/useShopping'
import { getUpcomingMeals } from '@/api/meals'
import { itemsToGroups, groupsToItems, uniqueMeals } from './shopping.js'
import { itemsToGroups, uniqueMeals } from './shopping.js'
import MealSelectionList from './MealSelectionList.vue'
import ShoppingListItem from './ShoppingListItem.vue'
async function saveShoppingList(outstandingItemGroups) {
const items = groupsToItems(outstandingItemGroups);
if (items.length === 0) {
alert.show({ type: 'error', message: 'No items selected.' });
return;
}
const router = useRouter()
const { getCurrentShoppingList, requestMeal, unrequestMeal, purchaseFromGroups } = useShopping()
return await purchaseShoppingList(items);
}
const from = new Date()
from.setTime(0)
const to = new Date()
to.setDate(to.getDate() + 7)
const shoppingList = ref(null)
const upcomingMeals = ref([])
const selected = ref([])
const showPurchased = ref(false)
const groupsMatch = (a, b) => {
if (!!a.product != !!b.product) return false;
if (a.name) return a.name === b.name;
return a.product.id === b.product.id;
if (!!a.product != !!b.product) return false
if (a.name) return a.name === b.name
return a.product.id === b.product.id
}
export default {
name: 'FullShoppingListPage',
components: { MealSelectionList, ShoppingListItem },
props: {
stockTaking: {
type: Boolean,
default: false
},
},
data() {
const from = new Date();
from.setTime(0);
const to = new Date();
to.setDate(to.getDate() + 7);
const outstandingItemGroups = computed(() => itemsToGroups(shoppingList.value?.outstanding_items ?? []))
const purchasedItemGroups = computed(() => itemsToGroups(shoppingList.value?.purchased_items ?? []))
const purchasedMeals = computed(() => uniqueMeals(shoppingList.value?.purchased_items ?? []))
const availableMeals = computed(() => {
const meals = { ...(shoppingList.value?.meals_lookup ?? {}) }
upcomingMeals.value?.forEach((m) => {
if (!meals[m.id]) meals[m.id] = m
})
return Object.values(meals)
.filter((m) => !m.purchase_date)
.sort((a, b) => a.suggested_date - b.suggested_date)
})
const includedMeals = computed(() => shoppingList.value?.requested_meals.map((m) => m.meal) ?? [])
return { from, to, shoppingList: null, selected: [], showPurchased: false }
},
async beforeMount() {
this.loadData();
},
computed: {
outstandingItemGroups() {
return itemsToGroups(this.shoppingList?.outstanding_items ?? []);
},
purchasedItemGroups() {
return itemsToGroups(this.shoppingList?.purchased_items ?? []);
},
purchasedMeals() {
return uniqueMeals(this.shoppingList?.purchased_items ?? []);
},
availableMeals() {
const meals = { ...this.shoppingList?.meals_lookup ?? {} };
this.upcomingMeals?.forEach(m => {
if (!meals[m.id]) {
meals[m.id] = m;
}
});
return Object.values(meals).filter(m => !m.purchase_date).sort((a, b) => a.suggested_date - b.suggested_date);
},
includedMeals() {
return this.shoppingList?.requested_meals.map(m => m.meal) ?? [];
}
},
methods: {
async loadData() {
this.upcomingMeals = await getUpcomingMeals(this.from, this.to);
this.shoppingList = await getCurrentShoppingList();
},
async mealSelected(meal) {
await requestMeal(meal.id);
await this.loadData();
},
async mealUnselected(meal) {
await unrequestMeal(meal.id);
await this.loadData();
},
async markFound() {
await saveShoppingList(this.selected);
this.selected = [];
await this.loadData();
},
async markPurchased() {
const shoppingList = await saveShoppingList(this.selected);
if (!shoppingList || !shoppingList.id) {
alert.show({ type: 'error', message: 'Failed to purchase.' });
return;
}
this.selected = [];
this.$router.push(`/shopping/${shoppingList.id}`);
},
toggleSelect(item) {
const index = this.selected.findIndex(i => groupsMatch(i, item));
if (index === -1)
this.selected.push(item);
else
this.selected.splice(index, 1);
},
isSelected(item) {
return this.selected.some(i => groupsMatch(i, item));
}
}
async function loadData() {
upcomingMeals.value = await getUpcomingMeals(from, to)
shoppingList.value = await getCurrentShoppingList()
}
async function mealSelected(meal) {
await requestMeal(meal.id)
await loadData()
}
async function mealUnselected(meal) {
await unrequestMeal(meal.id)
await loadData()
}
async function markFound() {
const result = await purchaseFromGroups(selected.value)
if (!result) {
alert.show({ type: 'error', message: 'No items selected.' })
return
}
selected.value = []
await loadData()
}
async function markPurchased() {
const shopping = await purchaseFromGroups(selected.value)
if (!shopping || !shopping.id) {
alert.show({ type: 'error', message: 'Failed to purchase.' })
return
}
selected.value = []
router.push(`/shopping/${shopping.id}`)
}
function toggleSelect(item) {
const index = selected.value.findIndex((i) => groupsMatch(i, item))
if (index === -1) selected.value.push(item)
else selected.value.splice(index, 1)
}
function isSelected(item) {
return selected.value.some((i) => groupsMatch(i, item))
}
onBeforeMount(loadData)
</script>

View file

@ -35,51 +35,47 @@
</style>
<script>
import { getMyShoppingList, saveMyShoppingList } from '@/api/shopping'
<script setup>
import { ref, onBeforeMount } from 'vue'
import { useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
import { useShopping } from '@/composables/useShopping'
import EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue'
export default {
name: 'MyShoppingpage',
components: { EditableIngredientsPanel },
data() {
return { ingredients: [], person: null }
},
async beforeMount() {
const { loadUser } = useAuth()
const person = await loadUser()
if (!person)
return this.$router.push({ name: 'login' });
this.person = person;
await this.updateShoppingList();
},
methods: {
async updateShoppingList(save = false) {
const new_ingredients = save ?
await saveMyShoppingList(this.ingredients) :
await getMyShoppingList();
const router = useRouter()
const { loadUser } = useAuth()
const { getMyShoppingList, saveMyShoppingList } = useShopping()
this.ingredients = new_ingredients;
},
addIngredient() {
this.ingredients = [{ id: -1 }, ...this.ingredients];
},
deleteIngredient(ingredient) {
this.ingredients = this.ingredients.filter(i => i !== ingredient);
},
updateIngredient(oldIngredient, newIngredient) {
this.ingredients = this.ingredients.map(source => source === oldIngredient ? newIngredient : source);
},
async onEditing(isStartingEdit) {
await this.updateShoppingList(!isStartingEdit);
const person = ref(null)
const ingredients = ref([])
if (isStartingEdit && this.ingredients.length === 0) {
this.addIngredient();
}
}
}
async function updateShoppingList(save = false) {
const newIngredients = save ? await saveMyShoppingList(ingredients.value) : await getMyShoppingList()
ingredients.value = newIngredients
}
function addIngredient() {
ingredients.value = [{ id: -1 }, ...ingredients.value]
}
function deleteIngredient(ingredient) {
ingredients.value = ingredients.value.filter((i) => i !== ingredient)
}
function updateIngredient(oldIngredient, newIngredient) {
ingredients.value = ingredients.value.map((source) => (source === oldIngredient ? newIngredient : source))
}
async function onEditing(isStartingEdit) {
await updateShoppingList(!isStartingEdit)
if (isStartingEdit && ingredients.value.length === 0) addIngredient()
}
onBeforeMount(async () => {
const u = await loadUser()
if (!u) return router.push({ name: 'login' })
person.value = u
await updateShoppingList()
})
</script>

View file

@ -29,42 +29,25 @@
</style>
<script>
<script setup>
import { ref, computed, onBeforeMount } from 'vue'
import { useRoute } from 'vue-router'
import { ago } from '@/dateformats.js'
import { getShoppingList } from '@/api/shopping'
import { itemsToGroups, uniqueMeals } from './shopping.js'
import MealSelectionList from './MealSelectionList.vue'
import ShoppingListItem from './ShoppingListItem.vue'
export default {
name: 'FullShoppingListPage',
components: { MealSelectionList, ShoppingListItem },
props: {
id: [String, Number]
},
computed: {
includedMeals() {
if (!this.shoppingList) return [];
return uniqueMeals(this.shoppingList.items);
},
listByProduct() {
return this.shoppingList ? itemsToGroups(this.shoppingList.items) : [];
}
},
data() {
return {
shoppingList: null,
}
},
async beforeMount() {
this.shoppingList = await getShoppingList(this.id);
},
methods: {
ago
}
}
const route = useRoute()
const shoppingList = ref(null)
const includedMeals = computed(() => (shoppingList.value ? uniqueMeals(shoppingList.value.items) : []))
const listByProduct = computed(() => (shoppingList.value ? itemsToGroups(shoppingList.value.items) : []))
onBeforeMount(async () => {
const idParam = route.params.id
const id = typeof idParam === 'string' ? parseInt(idParam) : idParam
shoppingList.value = await getShoppingList(id)
})
</script>

View file

@ -0,0 +1,27 @@
import {
getCurrentShoppingList,
getShoppingList,
purchaseShoppingList,
requestMeal,
unrequestMeal,
getMyShoppingList,
saveMyShoppingList,
} from '@/api/shopping'
import { groupsToItems } from '@/components/shopping/shopping.js'
export function useShopping() {
return {
getCurrentShoppingList,
getShoppingList,
purchaseShoppingList,
requestMeal,
unrequestMeal,
getMyShoppingList,
saveMyShoppingList,
async purchaseFromGroups(groups) {
const items = groupsToItems(groups)
if (!items?.length) return null
return purchaseShoppingList(items)
},
}
}