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

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:
Pantry Lead Agent
2026-02-09 13:03:00 +00:00
parent be2af1675a
commit 4834286005
8 changed files with 1059 additions and 21 deletions

View File

@@ -0,0 +1,131 @@
<template>
<div class="space-y-4">
<!-- Loading State -->
<div v-if="loading" class="text-center py-12">
<UIcon name="i-heroicons-arrow-path" class="w-8 h-8 text-gray-400 animate-spin mx-auto mb-2" />
<p class="text-gray-600">Loading inventory...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-12">
<UIcon name="i-heroicons-exclamation-triangle" class="w-12 h-12 text-red-500 mx-auto mb-4" />
<p class="text-red-600 mb-4">{{ error }}</p>
<UButton @click="loadInventory" color="gray">Retry</UButton>
</div>
<!-- Empty State -->
<div v-else-if="!items || items.length === 0" class="text-center py-12">
<UIcon name="i-heroicons-inbox" class="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 class="text-lg font-semibold text-gray-900 mb-2">
No items yet
</h3>
<p class="text-gray-600 mb-6">
Start by scanning a barcode or adding an item manually.
</p>
<div class="flex gap-2 justify-center">
<UButton
to="/scan"
color="primary"
icon="i-heroicons-qr-code"
>
Scan First Item
</UButton>
<UButton
@click="$emit('add-item')"
color="white"
icon="i-heroicons-plus"
>
Add Manually
</UButton>
</div>
</div>
<!-- Inventory Grid -->
<div v-else class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<InventoryCard
v-for="item in items"
:key="item.id"
:item="item"
@edit="$emit('edit-item', item)"
@delete="handleDelete(item.id)"
@update-quantity="handleQuantityUpdate"
/>
</div>
</div>
</template>
<script setup lang="ts">
const { getInventory, deleteInventoryItem, updateQuantity } = useInventory()
const props = defineProps<{
refresh?: boolean
}>()
const emit = defineEmits<{
'add-item': []
'edit-item': [item: any]
}>()
const items = ref<any[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const loadInventory = async () => {
loading.value = true
error.value = null
const { data, error: fetchError } = await getInventory()
if (fetchError) {
error.value = 'Failed to load inventory. Please try again.'
loading.value = false
return
}
items.value = data || []
loading.value = false
}
const handleDelete = async (id: string) => {
if (!confirm('Are you sure you want to delete this item?')) {
return
}
const { error: deleteError } = await deleteInventoryItem(id)
if (deleteError) {
alert('Failed to delete item')
return
}
// Remove from local list
items.value = items.value.filter(item => item.id !== id)
}
const handleQuantityUpdate = async (id: string, change: number) => {
const result = await updateQuantity(id, change)
if (result.error) {
alert('Failed to update quantity')
return
}
// Reload inventory after update
await loadInventory()
}
// Load on mount
onMounted(loadInventory)
// Watch for refresh prop
watch(() => props.refresh, (newVal) => {
if (newVal) {
loadInventory()
}
})
// Expose reload method
defineExpose({
reload: loadInventory
})
</script>