munch-ease-frontend/src/components/shopping/ShoppingListItem.vue

205 lines
6 KiB
Vue
Raw Normal View History

2024-05-11 06:03:29 +00:00
<template>
2025-10-18 02:08:22 +00:00
<!-- Show a card of the product with the total, taking up the available space and have an expanding section to view sources -->
<div class="shopping-list-item">
<img
:src="`${shoppingListItemGroup.product?.img_small ?? require('@/assets/missing-product.svg')}`"
class="product-image"
2025-10-18 03:16:34 +00:00
>
2024-05-11 06:03:29 +00:00
<div class="product-details">
2025-10-18 02:08:22 +00:00
<h3 class="header">
<strong>
<a
v-if="shoppingListItemGroup.product?.link"
:href="shoppingListItemGroup.product?.link"
2025-10-18 03:16:34 +00:00
>{{ shoppingListItemGroup.product?.name }}</a>
<span v-else>{{ shoppingListItemGroup.name }}</span> </strong>,
2025-10-18 02:08:22 +00:00
<small>
2025-10-18 03:16:34 +00:00
<span
v-for="(total, index) in remainingRequiredTotals"
:key="total.id"
>
2025-10-18 02:08:22 +00:00
<span v-if="index">, </span>
<span>{{ formatQuantity(total.quantity) }}&nbsp;{{ total.unit }}</span>
</span>
2025-10-18 03:16:34 +00:00
<span
v-if="purchased.length > 0"
class="found-marker partial"
> {{ getFriendlyDate(lastPurchased) }}</span>
2025-10-18 02:08:22 +00:00
</small>
</h3>
2025-10-18 03:16:34 +00:00
<p
v-if="required.length > 0"
class="sources"
>
2025-10-18 02:08:22 +00:00
<strong>Need: </strong>
2025-10-18 03:16:34 +00:00
<span
v-for="(source, index) in required"
:key="source.id"
>
2025-10-18 02:08:22 +00:00
<span v-if="index">, and </span>
<span v-if="source.person">
{{ formatQuantity(source.ingredient.quantity) }}&nbsp;{{ source.ingredient.unit }} for
{{ source.person.name }}
</span>
<span v-else-if="source.recipe">
{{ formatQuantity(source.ingredient.quantity) }}&nbsp;{{ source.ingredient.unit }} in
<router-link :to="`/recipes/${source.recipe.id}/`">{{
source.recipe.name
}}</router-link>
for
<router-link :to="`/meals/${source.meal.id}/`">{{
source.meal.suggested_date.toLocaleDateString('en-AU', {
weekday: 'long',
month: 'long',
day: 'numeric',
})
}}</router-link>
</span>
<span v-else-if="source.meal">
{{ source.ingredient.line }} for
<router-link :to="`/meals/${source.meal.id}/`">{{
source.meal.suggested_date.toLocaleDateString('en-AU', {
weekday: 'long',
month: 'long',
day: 'numeric',
})
}}</router-link>
</span>
</span>
</p>
<p v-if="purchased.length > 0">
<strong>Already found or purchased: </strong>
2025-10-18 03:16:34 +00:00
<span
v-for="(source, index) in purchased"
:key="source.id"
>
2025-10-18 02:08:22 +00:00
<span v-if="index">, and </span>
<span v-if="source.person">
{{ formatQuantity(source.ingredient.quantity) }}&nbsp;{{ source.ingredient.unit }} for
{{ source.person.name }}
</span>
<span v-else-if="source.recipe">
{{ formatQuantity(source.ingredient.quantity) }}&nbsp;{{ source.ingredient.unit }} in
<router-link :to="`/recipes/${source.recipe.id}/`">{{
source.recipe.name
}}</router-link>
for
<router-link :to="`/meals/${source.meal.id}/`">{{
source.meal.suggested_date.toLocaleDateString('en-AU', {
weekday: 'long',
month: 'long',
day: 'numeric',
})
}}</router-link>
</span>
<span v-else-if="source.meal">
{{ source.ingredient.line }} for
<router-link :to="`/meals/${source.meal.id}/`">{{
source.meal.suggested_date.toLocaleDateString('en-AU', {
weekday: 'long',
month: 'long',
day: 'numeric',
})
}}</router-link>
</span>
</span>
</p>
2024-05-11 06:03:29 +00:00
</div>
2025-10-18 02:08:22 +00:00
</div>
2024-05-11 06:03:29 +00:00
</template>
2025-10-18 03:16:34 +00:00
<script setup>
import { computed } from 'vue'
import { ago } from '@/dateformats.js'
import { calculateTotals } from '@/units.js'
const props = defineProps({
// { product: { ... }, OR name: 'string', shoppingListItems: [...] }
shoppingListItemGroup: { type: Object, required: true },
})
const remainingRequiredTotals = computed(() =>
calculateTotals(props.shoppingListItemGroup.shoppingListItems.map((item) => item.ingredient))
)
const required = computed(() =>
props.shoppingListItemGroup.shoppingListItems.filter((item) => !item.list_id)
)
const purchased = computed(() =>
props.shoppingListItemGroup.shoppingListItems.filter((item) => item.list_id)
)
const lastPurchased = computed(() => {
const purchasedItems = props.shoppingListItemGroup.shoppingListItems.filter((item) => item.list_id)
if (purchasedItems.length === 0) return null
return purchasedItems.reduce((latest, item) => {
const itemDate = item?.meal?.suggested_date || item?.created_at
return !latest || (itemDate && itemDate > latest) ? itemDate : latest
}, null)
})
function getFriendlyDate(date) {
if (!date) return ''
return ago(date)
}
function formatQuantity(quantity) {
const log10 = Math.log10(quantity)
if (log10 < 0) return quantity.toPrecision(2)
if (log10 < 1) return quantity.toFixed(1)
return quantity.toFixed(0)
}
</script>
2024-05-11 06:03:29 +00:00
<style scoped>
/* Show the product image to the left, then the product name and size to the right */
.shopping-list-item {
2025-10-18 02:08:22 +00:00
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1em;
2024-05-11 06:03:29 +00:00
}
.product-image {
2025-10-18 02:08:22 +00:00
max-width: 5em;
max-height: 4em;
margin-right: 1em;
2024-05-11 06:03:29 +00:00
}
.product-details {
2025-10-18 02:08:22 +00:00
flex-grow: 1;
text-align: left;
2024-05-11 06:03:29 +00:00
}
.header {
2025-10-18 02:08:22 +00:00
margin: 0;
2024-05-11 06:03:29 +00:00
}
2024-05-19 10:21:49 +00:00
.found-marker {
2025-10-18 02:08:22 +00:00
margin: 1ex;
padding-top: 0.6ex;
padding-bottom: 0.5ex;
padding-left: 1em;
padding-right: 1em;
border-radius: 25px;
text-align: center;
white-space: nowrap;
font-size: smaller;
color: white;
font-weight: bold;
z-index: 1000;
2024-05-19 10:21:49 +00:00
}
.found-marker.found {
2025-10-18 02:08:22 +00:00
background-color: green;
2024-05-19 10:21:49 +00:00
}
.found-marker.partial {
2025-10-18 02:08:22 +00:00
background-color: darkgoldenrod;
2024-05-19 10:21:49 +00:00
}
2024-05-11 06:03:29 +00:00
</style>