Refactor #5
This commit is contained in:
parent
8797a5489c
commit
2cc87e1891
5 changed files with 159 additions and 175 deletions
|
|
@ -32,8 +32,10 @@ Outcome: Feature modules call cohesive services; logic for mapping/normalization
|
||||||
|
|
||||||
### Phase 3 — Composables (UI-Facing Logic)
|
### Phase 3 — Composables (UI-Facing Logic)
|
||||||
- [x] Add `src/composables/useAuth.js` (user ref, ensureAuth)
|
- [x] Add `src/composables/useAuth.js` (user ref, ensureAuth)
|
||||||
- [ ] Add `src/composables/useMeals.js` (fetch and mutate meals)
|
- [x] Add `src/composables/useMeals.js` (fetch and mutate meals)
|
||||||
- [ ] Refactor pages to use composables and `<script setup>` where appropriate
|
- [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.
|
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 `api/auth.js` and `api/persons.js`; router uses auth API
|
||||||
- Added `composables/useAuth.js` and used in `MyShoppingPage.vue`
|
- Added `composables/useAuth.js` and used in `MyShoppingPage.vue`
|
||||||
- Added Prettier and EditorConfig
|
- 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>`
|
||||||
|
|
|
||||||
|
|
@ -133,119 +133,93 @@ button img {
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, onBeforeMount } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import alert from '@/alert.js'
|
import alert from '@/alert.js'
|
||||||
|
import { useShopping } from '@/composables/useShopping'
|
||||||
import { getCurrentShoppingList, purchaseShoppingList, requestMeal, unrequestMeal } from '@/api/shopping'
|
|
||||||
import { getUpcomingMeals } from '@/api/meals'
|
import { getUpcomingMeals } from '@/api/meals'
|
||||||
import { itemsToGroups, groupsToItems, uniqueMeals } from './shopping.js'
|
import { itemsToGroups, uniqueMeals } from './shopping.js'
|
||||||
|
|
||||||
import MealSelectionList from './MealSelectionList.vue'
|
import MealSelectionList from './MealSelectionList.vue'
|
||||||
import ShoppingListItem from './ShoppingListItem.vue'
|
import ShoppingListItem from './ShoppingListItem.vue'
|
||||||
|
|
||||||
async function saveShoppingList(outstandingItemGroups) {
|
const router = useRouter()
|
||||||
const items = groupsToItems(outstandingItemGroups);
|
const { getCurrentShoppingList, requestMeal, unrequestMeal, purchaseFromGroups } = useShopping()
|
||||||
if (items.length === 0) {
|
|
||||||
alert.show({ type: 'error', message: 'No items selected.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
const groupsMatch = (a, b) => {
|
||||||
if (!!a.product != !!b.product) return false;
|
if (!!a.product != !!b.product) return false
|
||||||
if (a.name) return a.name === b.name;
|
if (a.name) return a.name === b.name
|
||||||
return a.product.id === b.product.id;
|
return a.product.id === b.product.id
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
const outstandingItemGroups = computed(() => itemsToGroups(shoppingList.value?.outstanding_items ?? []))
|
||||||
name: 'FullShoppingListPage',
|
const purchasedItemGroups = computed(() => itemsToGroups(shoppingList.value?.purchased_items ?? []))
|
||||||
components: { MealSelectionList, ShoppingListItem },
|
const purchasedMeals = computed(() => uniqueMeals(shoppingList.value?.purchased_items ?? []))
|
||||||
props: {
|
const availableMeals = computed(() => {
|
||||||
stockTaking: {
|
const meals = { ...(shoppingList.value?.meals_lookup ?? {}) }
|
||||||
type: Boolean,
|
upcomingMeals.value?.forEach((m) => {
|
||||||
default: false
|
if (!meals[m.id]) meals[m.id] = m
|
||||||
},
|
})
|
||||||
},
|
return Object.values(meals)
|
||||||
data() {
|
.filter((m) => !m.purchase_date)
|
||||||
const from = new Date();
|
.sort((a, b) => a.suggested_date - b.suggested_date)
|
||||||
from.setTime(0);
|
})
|
||||||
|
const includedMeals = computed(() => shoppingList.value?.requested_meals.map((m) => m.meal) ?? [])
|
||||||
|
|
||||||
const to = new Date();
|
async function loadData() {
|
||||||
to.setDate(to.getDate() + 7);
|
upcomingMeals.value = await getUpcomingMeals(from, to)
|
||||||
|
shoppingList.value = await getCurrentShoppingList()
|
||||||
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 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>
|
</script>
|
||||||
|
|
@ -35,51 +35,47 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { getMyShoppingList, saveMyShoppingList } from '@/api/shopping'
|
import { ref, onBeforeMount } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useShopping } from '@/composables/useShopping'
|
||||||
import EditableIngredientsPanel from '@/components/ingredients/EditableIngredientsPanel.vue'
|
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;
|
const router = useRouter()
|
||||||
await this.updateShoppingList();
|
const { loadUser } = useAuth()
|
||||||
},
|
const { getMyShoppingList, saveMyShoppingList } = useShopping()
|
||||||
methods: {
|
|
||||||
async updateShoppingList(save = false) {
|
|
||||||
const new_ingredients = save ?
|
|
||||||
await saveMyShoppingList(this.ingredients) :
|
|
||||||
await getMyShoppingList();
|
|
||||||
|
|
||||||
this.ingredients = new_ingredients;
|
const person = ref(null)
|
||||||
},
|
const ingredients = ref([])
|
||||||
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);
|
|
||||||
|
|
||||||
if (isStartingEdit && this.ingredients.length === 0) {
|
async function updateShoppingList(save = false) {
|
||||||
this.addIngredient();
|
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>
|
</script>
|
||||||
|
|
@ -29,42 +29,25 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { ref, computed, onBeforeMount } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import { ago } from '@/dateformats.js'
|
import { ago } from '@/dateformats.js'
|
||||||
|
|
||||||
import { getShoppingList } from '@/api/shopping'
|
import { getShoppingList } from '@/api/shopping'
|
||||||
import { itemsToGroups, uniqueMeals } from './shopping.js'
|
import { itemsToGroups, uniqueMeals } from './shopping.js'
|
||||||
|
|
||||||
import MealSelectionList from './MealSelectionList.vue'
|
import MealSelectionList from './MealSelectionList.vue'
|
||||||
import ShoppingListItem from './ShoppingListItem.vue'
|
import ShoppingListItem from './ShoppingListItem.vue'
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
name: 'FullShoppingListPage',
|
const shoppingList = ref(null)
|
||||||
components: { MealSelectionList, ShoppingListItem },
|
|
||||||
props: {
|
const includedMeals = computed(() => (shoppingList.value ? uniqueMeals(shoppingList.value.items) : []))
|
||||||
id: [String, Number]
|
const listByProduct = computed(() => (shoppingList.value ? itemsToGroups(shoppingList.value.items) : []))
|
||||||
},
|
|
||||||
computed: {
|
onBeforeMount(async () => {
|
||||||
includedMeals() {
|
const idParam = route.params.id
|
||||||
if (!this.shoppingList) return [];
|
const id = typeof idParam === 'string' ? parseInt(idParam) : idParam
|
||||||
return uniqueMeals(this.shoppingList.items);
|
shoppingList.value = await getShoppingList(id)
|
||||||
},
|
})
|
||||||
listByProduct() {
|
|
||||||
return this.shoppingList ? itemsToGroups(this.shoppingList.items) : [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
shoppingList: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async beforeMount() {
|
|
||||||
this.shoppingList = await getShoppingList(this.id);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
ago
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
27
src/composables/useShopping.js
Normal file
27
src/composables/useShopping.js
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue