diff --git a/app/components/inventory/AddItemForm.vue b/app/components/inventory/AddItemForm.vue new file mode 100644 index 0000000..72d8137 --- /dev/null +++ b/app/components/inventory/AddItemForm.vue @@ -0,0 +1,257 @@ + + + diff --git a/app/components/inventory/EditItemModal.vue b/app/components/inventory/EditItemModal.vue new file mode 100644 index 0000000..2d4e270 --- /dev/null +++ b/app/components/inventory/EditItemModal.vue @@ -0,0 +1,184 @@ + + + diff --git a/app/components/inventory/InventoryCard.vue b/app/components/inventory/InventoryCard.vue new file mode 100644 index 0000000..6e7901a --- /dev/null +++ b/app/components/inventory/InventoryCard.vue @@ -0,0 +1,151 @@ + + + diff --git a/app/components/inventory/InventoryList.vue b/app/components/inventory/InventoryList.vue new file mode 100644 index 0000000..50bdf50 --- /dev/null +++ b/app/components/inventory/InventoryList.vue @@ -0,0 +1,131 @@ + + + diff --git a/app/composables/useInventory.ts b/app/composables/useInventory.ts new file mode 100644 index 0000000..90d4a79 --- /dev/null +++ b/app/composables/useInventory.ts @@ -0,0 +1,201 @@ +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) => { + 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 + } +} diff --git a/app/composables/useTags.ts b/app/composables/useTags.ts new file mode 100644 index 0000000..ed7bfa5 --- /dev/null +++ b/app/composables/useTags.ts @@ -0,0 +1,44 @@ +export const useTags = () => { + const supabase = useSupabase() + + /** + * Get all tags + */ + const getTags = async () => { + const { data, error } = await supabase + .from('tags') + .select('*') + .order('category', { ascending: true }) + .order('name', { ascending: true }) + + if (error) { + console.error('Error fetching tags:', error) + return { data: null, error } + } + + return { data, error: null } + } + + /** + * Get tags by category + */ + const getTagsByCategory = async (category: 'position' | 'type' | 'dietary' | 'custom') => { + const { data, error } = await supabase + .from('tags') + .select('*') + .eq('category', category) + .order('name', { ascending: true }) + + if (error) { + console.error('Error fetching tags by category:', error) + return { data: null, error } + } + + return { data, error: null } + } + + return { + getTags, + getTagsByCategory + } +} diff --git a/app/composables/useUnits.ts b/app/composables/useUnits.ts new file mode 100644 index 0000000..b99ac2d --- /dev/null +++ b/app/composables/useUnits.ts @@ -0,0 +1,53 @@ +export const useUnits = () => { + const supabase = useSupabase() + + /** + * Get all units + */ + const getUnits = async () => { + const { data, error } = await supabase + .from('units') + .select('*') + .order('unit_type', { ascending: true }) + .order('name', { ascending: true }) + + if (error) { + console.error('Error fetching units:', error) + return { data: null, error } + } + + return { data, error: null } + } + + /** + * Get default unit for a type + */ + const getDefaultUnit = async (unitType: 'weight' | 'volume' | 'count' | 'custom') => { + const { data, error } = await supabase + .from('units') + .select('*') + .eq('unit_type', unitType) + .eq('is_default', true) + .single() + + if (error) { + console.error('Error fetching default unit:', error) + return { data: null, error } + } + + return { data, error: null } + } + + /** + * Convert quantity between units + */ + const convertUnit = (quantity: number, fromFactor: number, toFactor: number): number => { + return (quantity * fromFactor) / toFactor + } + + return { + getUnits, + getDefaultUnit, + convertUnit + } +} diff --git a/app/pages/index.vue b/app/pages/index.vue index 7211247..7222020 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -17,36 +17,37 @@ color="white" size="lg" icon="i-heroicons-plus" + @click="showAddForm = true" > Add Manually - - -
- +
+
+ -

- No items yet -

-

- Start by scanning a barcode or adding an item manually. -

- - Scan First Item -
- +
- + + + + +
@@ -54,4 +55,20 @@ definePageMeta({ layout: 'default' }) + +const showAddForm = ref(false) +const editingItem = ref(null) +const refreshKey = ref(0) +const inventoryListRef = ref() + +const handleItemAdded = (item: any) => { + showAddForm.value = false + // Reload the inventory list + inventoryListRef.value?.reload() +} + +const handleItemUpdated = (item: any) => { + editingItem.value = null + inventoryListRef.value?.reload() +}