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"
|
|
|
|
|
/>
|
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"
|
|
|
|
|
>{{ shoppingListItemGroup.product?.name }}</a
|
|
|
|
|
>
|
|
|
|
|
<span v-else>{{ shoppingListItemGroup.name }}</span> </strong
|
|
|
|
|
>,
|
|
|
|
|
<small>
|
|
|
|
|
<span v-for="(total, index) in remainingRequiredTotals" :key="total.id">
|
|
|
|
|
<span v-if="index">, </span>
|
|
|
|
|
<span>{{ formatQuantity(total.quantity) }} {{ total.unit }}</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="found-marker partial" v-if="purchased.length > 0"
|
|
|
|
|
>✓ {{ getFriendlyDate(lastPurchased) }}</span
|
|
|
|
|
>
|
|
|
|
|
</small>
|
|
|
|
|
</h3>
|
|
|
|
|
<p class="sources" v-if="required.length > 0">
|
|
|
|
|
<strong>Need: </strong>
|
|
|
|
|
<span v-for="(source, index) in required" :key="source.id">
|
|
|
|
|
<span v-if="index">, and </span>
|
|
|
|
|
<span v-if="source.person">
|
|
|
|
|
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for
|
|
|
|
|
{{ source.person.name }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else-if="source.recipe">
|
|
|
|
|
{{ formatQuantity(source.ingredient.quantity) }} {{ 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>
|
|
|
|
|
<span v-for="(source, index) in purchased" :key="source.id">
|
|
|
|
|
<span v-if="index">, and </span>
|
|
|
|
|
<span v-if="source.person">
|
|
|
|
|
{{ formatQuantity(source.ingredient.quantity) }} {{ source.ingredient.unit }} for
|
|
|
|
|
{{ source.person.name }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else-if="source.recipe">
|
|
|
|
|
{{ formatQuantity(source.ingredient.quantity) }} {{ 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>
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
2025-10-18 02:53:24 +00:00
|
|
|
<script setup>
|
|
|
|
|
import { computed } from 'vue'
|
2025-10-18 02:08:22 +00:00
|
|
|
import { ago } from '@/dateformats.js'
|
|
|
|
|
import { calculateTotals } from '@/units.js'
|
2024-10-13 08:19:36 +00:00
|
|
|
|
2025-10-18 02:53:24 +00:00
|
|
|
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)
|
2024-05-11 06:03:29 +00:00
|
|
|
}
|
2025-10-18 02:08:22 +00:00
|
|
|
</script>
|