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
202 lines
4.4 KiB
TypeScript
202 lines
4.4 KiB
TypeScript
import type { Database } from '~/types/database.types'
|
|
|
|
type InventoryItem = Database['public']['Tables']['inventory_items']['Row']
|
|
type InventoryItemInsert = Database['public']['Tables']['inventory_items']['Insert']
|
|
type InventoryItemUpdate = Database['public']['Tables']['inventory_items']['Update']
|
|
|
|
export const useInventory = () => {
|
|
const supabase = useSupabase()
|
|
const { user } = useSupabaseAuth()
|
|
|
|
/**
|
|
* Get all inventory items with denormalized data
|
|
*/
|
|
const getInventory = async () => {
|
|
const { data, error } = await supabase
|
|
.from('inventory_items')
|
|
.select(`
|
|
*,
|
|
product:products(*),
|
|
unit:units(*),
|
|
tags:item_tags(tag:tags(*))
|
|
`)
|
|
.order('created_at', { ascending: false })
|
|
|
|
if (error) {
|
|
console.error('Error fetching inventory:', error)
|
|
return { data: null, error }
|
|
}
|
|
|
|
return { data, error: null }
|
|
}
|
|
|
|
/**
|
|
* Get single inventory item by ID
|
|
*/
|
|
const getInventoryItem = async (id: string) => {
|
|
const { data, error } = await supabase
|
|
.from('inventory_items')
|
|
.select(`
|
|
*,
|
|
product:products(*),
|
|
unit:units(*),
|
|
tags:item_tags(tag:tags(*))
|
|
`)
|
|
.eq('id', id)
|
|
.single()
|
|
|
|
if (error) {
|
|
console.error('Error fetching item:', error)
|
|
return { data: null, error }
|
|
}
|
|
|
|
return { data, error: null }
|
|
}
|
|
|
|
/**
|
|
* Add new inventory item
|
|
*/
|
|
const addInventoryItem = async (item: Omit<InventoryItemInsert, 'added_by'>) => {
|
|
if (!user.value) {
|
|
return { data: null, error: { message: 'User not authenticated' } }
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from('inventory_items')
|
|
.insert({
|
|
...item,
|
|
added_by: user.value.id
|
|
})
|
|
.select(`
|
|
*,
|
|
product:products(*),
|
|
unit:units(*),
|
|
tags:item_tags(tag:tags(*))
|
|
`)
|
|
.single()
|
|
|
|
if (error) {
|
|
console.error('Error adding item:', error)
|
|
return { data: null, error }
|
|
}
|
|
|
|
return { data, error: null }
|
|
}
|
|
|
|
/**
|
|
* Update inventory item
|
|
*/
|
|
const updateInventoryItem = async (id: string, updates: InventoryItemUpdate) => {
|
|
const { data, error } = await supabase
|
|
.from('inventory_items')
|
|
.update(updates)
|
|
.eq('id', id)
|
|
.select(`
|
|
*,
|
|
product:products(*),
|
|
unit:units(*),
|
|
tags:item_tags(tag:tags(*))
|
|
`)
|
|
.single()
|
|
|
|
if (error) {
|
|
console.error('Error updating item:', error)
|
|
return { data: null, error }
|
|
}
|
|
|
|
return { data, error: null }
|
|
}
|
|
|
|
/**
|
|
* Delete inventory item
|
|
*/
|
|
const deleteInventoryItem = async (id: string) => {
|
|
// First delete associated tags
|
|
await supabase
|
|
.from('item_tags')
|
|
.delete()
|
|
.eq('item_id', id)
|
|
|
|
const { error } = await supabase
|
|
.from('inventory_items')
|
|
.delete()
|
|
.eq('id', id)
|
|
|
|
if (error) {
|
|
console.error('Error deleting item:', error)
|
|
return { error }
|
|
}
|
|
|
|
return { error: null }
|
|
}
|
|
|
|
/**
|
|
* Update item quantity (consume or restock)
|
|
*/
|
|
const updateQuantity = async (id: string, change: number) => {
|
|
const { data: item, error: fetchError } = await getInventoryItem(id)
|
|
if (fetchError || !item) {
|
|
return { data: null, error: fetchError }
|
|
}
|
|
|
|
const newQuantity = Number(item.quantity) + change
|
|
|
|
if (newQuantity <= 0) {
|
|
// Auto-delete when quantity reaches zero
|
|
return await deleteInventoryItem(id)
|
|
}
|
|
|
|
return await updateInventoryItem(id, { quantity: newQuantity })
|
|
}
|
|
|
|
/**
|
|
* Add tags to item
|
|
*/
|
|
const addItemTags = async (itemId: string, tagIds: string[]) => {
|
|
const items = tagIds.map(tagId => ({
|
|
item_id: itemId,
|
|
tag_id: tagId
|
|
}))
|
|
|
|
const { error } = await supabase
|
|
.from('item_tags')
|
|
.insert(items)
|
|
|
|
if (error) {
|
|
console.error('Error adding tags:', error)
|
|
return { error }
|
|
}
|
|
|
|
return { error: null }
|
|
}
|
|
|
|
/**
|
|
* Remove tag from item
|
|
*/
|
|
const removeItemTag = async (itemId: string, tagId: string) => {
|
|
const { error } = await supabase
|
|
.from('item_tags')
|
|
.delete()
|
|
.eq('item_id', itemId)
|
|
.eq('tag_id', tagId)
|
|
|
|
if (error) {
|
|
console.error('Error removing tag:', error)
|
|
return { error }
|
|
}
|
|
|
|
return { error: null }
|
|
}
|
|
|
|
return {
|
|
getInventory,
|
|
getInventoryItem,
|
|
addInventoryItem,
|
|
updateInventoryItem,
|
|
deleteInventoryItem,
|
|
updateQuantity,
|
|
addItemTags,
|
|
removeItemTag
|
|
}
|
|
}
|