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:
131
app/components/inventory/InventoryList.vue
Normal file
131
app/components/inventory/InventoryList.vue
Normal 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>
|
||||
Reference in New Issue
Block a user