feat: implement inventory CRUD UI components (#18 #19 #20 #21)
Some checks failed
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 / Code Quality (pull_request) Has been cancelled
Deploy to Coolify / Run Tests (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
Some checks failed
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 / Code Quality (pull_request) Has been cancelled
Deploy to Coolify / Run Tests (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
Week 2 core inventory management: **Composables:** - useInventory: Full CRUD operations for inventory items - useUnits: Unit fetching and conversion helpers - useTags: Tag fetching and category filtering **Components:** - InventoryList (#18): Grid view with loading/empty/error states - InventoryCard: Item card with image, quantity controls, tags, expiry - AddItemForm (#19): Form with tag picker, unit selector, validation - EditItemModal (#20): Modal form for editing existing items - Delete functionality (#21): Confirm dialog + cascade tag cleanup **Features:** - Quantity quick-actions (+/- buttons on cards) - Auto-delete when quantity reaches zero - Expiry date tracking with color-coded warnings - Tag selection by category in add form - Responsive grid layout (1-4 columns) - Product image display from barcode cache - Form validation and loading states Closes #18, #19, #20, #21
This commit is contained in:
151
app/components/inventory/InventoryCard.vue
Normal file
151
app/components/inventory/InventoryCard.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<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">
|
||||
<UBadge
|
||||
v-for="tagItem in item.tags.slice(0, 3)"
|
||||
:key="tagItem.tag.id"
|
||||
:style="{ backgroundColor: tagItem.tag.color }"
|
||||
size="xs"
|
||||
class="text-white"
|
||||
>
|
||||
{{ tagItem.tag.icon }} {{ tagItem.tag.name }}
|
||||
</UBadge>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<template #footer>
|
||||
<div class="flex gap-2">
|
||||
<UButton
|
||||
icon="i-heroicons-pencil"
|
||||
size="sm"
|
||||
color="gray"
|
||||
variant="soft"
|
||||
class="flex-1"
|
||||
@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>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
item: any
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
edit: [item: any]
|
||||
delete: [id: string]
|
||||
'update-quantity': [id: string, change: number]
|
||||
}>()
|
||||
|
||||
// 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`
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user