Some checks failed
Deploy to Coolify / Code Quality (pull_request) Has been cancelled
Deploy to Coolify / Run Tests (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Development (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Production (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Test (pull_request) Has been cancelled
Pull Request Checks / Validate PR (pull_request) Has been cancelled
270 lines
7.6 KiB
Vue
270 lines
7.6 KiB
Vue
<template>
|
|
<UCard class="hover:shadow-lg transition-shadow">
|
|
<!-- Item Image -->
|
|
<div class="aspect-square bg-gray-100 rounded-lg mb-3 overflow-hidden">
|
|
<img
|
|
v-if="item.product?.image_url"
|
|
:src="item.product.image_url"
|
|
:alt="item.name"
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
<div v-else class="w-full h-full flex items-center justify-center">
|
|
<UIcon name="i-heroicons-cube" class="w-16 h-16 text-gray-300" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Item Info -->
|
|
<div class="space-y-2">
|
|
<div>
|
|
<h3 class="font-semibold text-gray-900 truncate">{{ item.name }}</h3>
|
|
<p v-if="item.product?.brand" class="text-sm text-gray-600 truncate">
|
|
{{ item.product.brand }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Quantity -->
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-lg font-medium text-gray-900">
|
|
{{ item.quantity }} {{ item.unit?.abbreviation }}
|
|
</span>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="flex gap-1">
|
|
<UButton
|
|
icon="i-heroicons-minus"
|
|
size="xs"
|
|
color="gray"
|
|
variant="ghost"
|
|
@click="$emit('update-quantity', item.id, -1)"
|
|
:disabled="item.quantity <= 1"
|
|
/>
|
|
<UButton
|
|
icon="i-heroicons-plus"
|
|
size="xs"
|
|
color="gray"
|
|
variant="ghost"
|
|
@click="$emit('update-quantity', item.id, 1)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div v-if="item.tags && item.tags.length > 0" class="flex flex-wrap gap-1">
|
|
<TagsTagBadge
|
|
v-for="tagItem in item.tags.slice(0, 3)"
|
|
:key="tagItem.tag.id"
|
|
:tag="tagItem.tag"
|
|
size="sm"
|
|
/>
|
|
<UBadge v-if="item.tags.length > 3" size="xs" color="gray">
|
|
+{{ item.tags.length - 3 }}
|
|
</UBadge>
|
|
</div>
|
|
|
|
<!-- Expiry Warning -->
|
|
<div v-if="daysUntilExpiry !== null" class="text-xs">
|
|
<UBadge
|
|
:color="expiryColor"
|
|
variant="soft"
|
|
class="w-full justify-center"
|
|
>
|
|
<UIcon :name="expiryIcon" class="mr-1" />
|
|
{{ expiryText }}
|
|
</UBadge>
|
|
</div>
|
|
|
|
<!-- Low Stock Warning -->
|
|
<div v-if="isLowStock" class="text-xs">
|
|
<UBadge
|
|
color="orange"
|
|
variant="soft"
|
|
class="w-full justify-center"
|
|
>
|
|
<UIcon name="i-heroicons-exclamation-triangle" class="mr-1" />
|
|
Low stock ({{ item.quantity }}/{{ item.low_stock_threshold }})
|
|
</UBadge>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<template #footer>
|
|
<div class="flex flex-col gap-2">
|
|
<!-- Quick Actions Row -->
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<UButton
|
|
icon="i-heroicons-arrow-trending-down"
|
|
size="sm"
|
|
color="orange"
|
|
variant="soft"
|
|
@click="handleConsume"
|
|
:disabled="item.quantity <= 0.01"
|
|
>
|
|
Consume
|
|
</UButton>
|
|
<UButton
|
|
icon="i-heroicons-arrow-trending-up"
|
|
size="sm"
|
|
color="green"
|
|
variant="soft"
|
|
@click="showRestockModal = true"
|
|
>
|
|
Restock
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- Management Actions Row -->
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<UButton
|
|
icon="i-heroicons-pencil"
|
|
size="sm"
|
|
color="gray"
|
|
variant="soft"
|
|
@click="$emit('edit', item)"
|
|
>
|
|
Edit
|
|
</UButton>
|
|
<UButton
|
|
icon="i-heroicons-trash"
|
|
size="sm"
|
|
color="red"
|
|
variant="soft"
|
|
@click="$emit('delete', item.id)"
|
|
>
|
|
Delete
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Restock Modal -->
|
|
<UModal v-model="showRestockModal">
|
|
<UCard>
|
|
<template #header>
|
|
<h3 class="text-lg font-semibold">Restock {{ item.name }}</h3>
|
|
</template>
|
|
|
|
<div class="space-y-4">
|
|
<div class="text-sm text-gray-600">
|
|
Current: <span class="font-semibold">{{ item.quantity }} {{ item.unit?.abbreviation }}</span>
|
|
</div>
|
|
|
|
<UFormGroup label="Amount to add">
|
|
<UInput
|
|
v-model.number="restockAmount"
|
|
type="number"
|
|
min="0.01"
|
|
step="0.01"
|
|
size="lg"
|
|
autofocus
|
|
placeholder="e.g. 5"
|
|
/>
|
|
</UFormGroup>
|
|
|
|
<div v-if="restockAmount > 0" class="text-sm text-gray-600">
|
|
New total: <span class="font-semibold">{{ (Number(item.quantity) + Number(restockAmount)).toFixed(2) }} {{ item.unit?.abbreviation }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex gap-2">
|
|
<UButton
|
|
color="primary"
|
|
size="lg"
|
|
class="flex-1"
|
|
@click="handleRestock"
|
|
:disabled="!restockAmount || restockAmount <= 0"
|
|
>
|
|
Add {{ restockAmount || 0 }} {{ item.unit?.abbreviation }}
|
|
</UButton>
|
|
<UButton
|
|
color="gray"
|
|
size="lg"
|
|
variant="soft"
|
|
@click="showRestockModal = false"
|
|
>
|
|
Cancel
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
</UModal>
|
|
</UCard>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const props = defineProps<{
|
|
item: any
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
edit: [item: any]
|
|
delete: [id: string]
|
|
'update-quantity': [id: string, change: number]
|
|
'consume': [id: string]
|
|
'restock': [id: string, amount: number]
|
|
}>()
|
|
|
|
const showRestockModal = ref(false)
|
|
const restockAmount = ref<number>(1)
|
|
|
|
// Calculate days until expiry
|
|
const daysUntilExpiry = computed(() => {
|
|
if (!props.item.expiry_date) return null
|
|
|
|
const today = new Date()
|
|
const expiry = new Date(props.item.expiry_date)
|
|
const diff = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
|
|
|
|
return diff
|
|
})
|
|
|
|
// Expiry badge styling
|
|
const expiryColor = computed(() => {
|
|
if (daysUntilExpiry.value === null) return 'gray'
|
|
if (daysUntilExpiry.value < 0) return 'red'
|
|
if (daysUntilExpiry.value <= 3) return 'orange'
|
|
if (daysUntilExpiry.value <= 7) return 'yellow'
|
|
return 'green'
|
|
})
|
|
|
|
const expiryIcon = computed(() => {
|
|
if (daysUntilExpiry.value === null) return 'i-heroicons-calendar'
|
|
if (daysUntilExpiry.value < 0) return 'i-heroicons-exclamation-triangle'
|
|
return 'i-heroicons-clock'
|
|
})
|
|
|
|
const expiryText = computed(() => {
|
|
if (daysUntilExpiry.value === null) return 'No expiry'
|
|
if (daysUntilExpiry.value < 0) return `Expired ${Math.abs(daysUntilExpiry.value)} days ago`
|
|
if (daysUntilExpiry.value === 0) return 'Expires today'
|
|
if (daysUntilExpiry.value === 1) return 'Expires tomorrow'
|
|
return `Expires in ${daysUntilExpiry.value} days`
|
|
})
|
|
|
|
// Low stock detection
|
|
const isLowStock = computed(() => {
|
|
if (!props.item.low_stock_threshold) return false
|
|
return Number(props.item.quantity) <= Number(props.item.low_stock_threshold)
|
|
})
|
|
|
|
// Quick actions
|
|
const handleConsume = () => {
|
|
emit('update-quantity', props.item.id, -1)
|
|
}
|
|
|
|
const handleRestock = () => {
|
|
if (restockAmount.value && restockAmount.value > 0) {
|
|
emit('update-quantity', props.item.id, restockAmount.value)
|
|
showRestockModal.value = false
|
|
restockAmount.value = 1 // Reset for next time
|
|
}
|
|
}
|
|
|
|
// Reset restock amount when modal closes
|
|
watch(showRestockModal, (isOpen) => {
|
|
if (!isOpen) {
|
|
restockAmount.value = 1
|
|
}
|
|
})
|
|
</script>
|