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 ) } } & nbsp ; { { total . unit } } < / span >
< / span >
< span class = "found-marker partial" v-if = "purchased.length > 0"
> ✓ { { getFriendlyDate ( lastPurchased ) } } < / s p a n
>
< / 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 ) } } & 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 >
< 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 ) } } & 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 >
< 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 : 1 em ;
2024-05-11 06:03:29 +00:00
}
. product - image {
2025-10-18 02:08:22 +00:00
max - width : 5 em ;
max - height : 4 em ;
margin - right : 1 em ;
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 : 1 ex ;
padding - top : 0.6 ex ;
padding - bottom : 0.5 ex ;
padding - left : 1 em ;
padding - right : 1 em ;
border - radius : 25 px ;
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 >
< script >
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
2024-05-11 06:03:29 +00:00
export default {
2025-10-18 02:08:22 +00:00
name : 'ShoppingListItem' ,
props : [ 'shoppingListItemGroup' ] , // { product: { ... }, OR name: 'string', shoppingListItems: { person, ingredient, list_id?, meal? }} where list_id is null if not yet purchased
data ( ) {
return {
expanded : false ,
}
} ,
computed : {
remainingRequiredTotals ( ) {
return calculateTotals (
this . shoppingListItemGroup . shoppingListItems . map ( ( item ) => item . ingredient )
)
2024-05-11 06:03:29 +00:00
} ,
2025-10-18 02:08:22 +00:00
expectedExistingTotals ( ) {
const purchasedNotEaten = this . shoppingListItemGroup . shoppingListItems . filter (
( item ) => item . list _id && ! item ? . meal ? . consumed _date
)
2025-07-28 23:22:58 +00:00
2025-10-18 02:08:22 +00:00
return calculateTotals ( purchasedNotEaten . map ( ( item ) => item . ingredient ) )
2024-10-13 08:19:36 +00:00
} ,
2025-10-18 02:08:22 +00:00
required ( ) {
return this . shoppingListItemGroup . shoppingListItems . filter ( ( item ) => ! item . list _id )
} ,
purchased ( ) {
return this . shoppingListItemGroup . shoppingListItems . filter ( ( item ) => item . list _id )
} ,
lastPurchased ( ) {
const purchasedItems = this . shoppingListItemGroup . shoppingListItems . filter (
( item ) => item . list _id
)
if ( purchasedItems . length === 0 ) {
return null
}
// Find the most recently purchased item
return purchasedItems . reduce ( ( latest , item ) => {
const itemDate = item ? . meal ? . suggested _date || item ? . created _at
return ! latest || ( itemDate && itemDate > latest ) ? itemDate : latest
} , null )
} ,
} ,
methods : {
getFriendlyDate ( date ) {
if ( ! date ) return ''
2024-05-19 10:21:49 +00:00
2025-10-18 02:08:22 +00:00
return ago ( date )
} ,
formatQuantity ( quantity ) {
const log10 = Math . log10 ( quantity )
if ( log10 < 0 ) {
return quantity . toPrecision ( 2 )
} else if ( log10 < 1 ) {
return quantity . toFixed ( 1 )
} else {
return quantity . toFixed ( 0 )
}
} ,
} ,
2024-05-11 06:03:29 +00:00
}
2025-10-18 02:08:22 +00:00
< / script >