From 7d35a3e7b307447659d1f4dac67a412b927f9e7d Mon Sep 17 00:00:00 2001 From: Pantry Lead Agent Date: Tue, 24 Feb 2026 00:04:10 +0000 Subject: [PATCH] feat: implement scan-to-add flow (#25) - Create useProductLookup composable - Integrate real product lookup in scan page - Add query parameter handling in index.vue - Pre-fill AddItemForm from scan data - Parse quantity and unit from product data - Include barcode and brand in notes Complete end-to-end scan workflow: 1. Scan barcode 2. Fetch from Open Food Facts 3. Navigate to inventory with data 4. Pre-filled add form 5. One-click add to inventory Closes #25 --- app/components/inventory/AddItemForm.vue | 44 +++++++++++++++++ app/composables/useProductLookup.ts | 61 ++++++++++++++++++++++++ app/pages/index.vue | 32 ++++++++++++- app/pages/scan.vue | 46 ++++++------------ 4 files changed, 151 insertions(+), 32 deletions(-) create mode 100644 app/composables/useProductLookup.ts diff --git a/app/components/inventory/AddItemForm.vue b/app/components/inventory/AddItemForm.vue index 72d8137..3fe8082 100644 --- a/app/components/inventory/AddItemForm.vue +++ b/app/components/inventory/AddItemForm.vue @@ -131,6 +131,16 @@ const { addInventoryItem, addItemTags } = useInventory() const { getUnits } = useUnits() const { getTags } = useTags() +const props = defineProps<{ + initialData?: { + barcode?: string + name?: string + brand?: string + image_url?: string + quantity?: string + } +}>() + const emit = defineEmits<{ close: [] added: [item: any] @@ -166,6 +176,40 @@ onMounted(async () => { if (defaultUnit) { form.unit_id = defaultUnit.id } + + // Pre-fill from initial data (scan-to-add flow) + if (props.initialData) { + if (props.initialData.name) { + form.name = props.initialData.name + } + + // Add brand to notes if available + if (props.initialData.brand) { + form.notes = `Brand: ${props.initialData.brand}` + + if (props.initialData.barcode) { + form.notes += `\nBarcode: ${props.initialData.barcode}` + } + } else if (props.initialData.barcode) { + form.notes = `Barcode: ${props.initialData.barcode}` + } + + // Parse quantity if available (e.g., "750g") + if (props.initialData.quantity) { + const quantityMatch = props.initialData.quantity.match(/^([\d.]+)\s*([a-zA-Z]+)$/) + if (quantityMatch) { + form.quantity = parseFloat(quantityMatch[1]) + // Try to match unit + const unitAbbr = quantityMatch[2].toLowerCase() + const matchedUnit = units.value.find(u => + u.abbreviation.toLowerCase() === unitAbbr + ) + if (matchedUnit) { + form.unit_id = matchedUnit.id + } + } + } + } }) // Unit options for select diff --git a/app/composables/useProductLookup.ts b/app/composables/useProductLookup.ts new file mode 100644 index 0000000..647fcc7 --- /dev/null +++ b/app/composables/useProductLookup.ts @@ -0,0 +1,61 @@ +// Composable for product lookup via Edge Function + +export interface ProductData { + barcode: string + name: string + brand?: string + quantity?: string + image_url?: string + category?: string + cached?: boolean +} + +export const useProductLookup = () => { + const supabase = useSupabaseClient() + const isLoading = ref(false) + const error = ref(null) + + const lookupProduct = async (barcode: string): Promise => { + isLoading.value = true + error.value = null + + try { + const { data, error: functionError } = await supabase.functions.invoke('product-lookup', { + body: { barcode } + }) + + if (functionError) { + console.error('Product lookup error:', functionError) + error.value = functionError.message || 'Failed to lookup product' + + // Return basic product data even on error + return { + barcode, + name: `Product ${barcode}`, + cached: false + } + } + + return data as ProductData + + } catch (err) { + console.error('Unexpected error during product lookup:', err) + error.value = err instanceof Error ? err.message : 'Unknown error' + + // Return basic product data even on error + return { + barcode, + name: `Product ${barcode}`, + cached: false + } + } finally { + isLoading.value = false + } + } + + return { + lookupProduct, + isLoading: readonly(isLoading), + error: readonly(error) + } +} diff --git a/app/pages/index.vue b/app/pages/index.vue index 7222020..0ed0dc3 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -28,7 +28,8 @@
@@ -56,13 +57,42 @@ definePageMeta({ layout: 'default' }) +const route = useRoute() +const router = useRouter() + const showAddForm = ref(false) const editingItem = ref(null) const refreshKey = ref(0) const inventoryListRef = ref() +const prefilledData = ref(null) + +// Handle scan-to-add flow (Issue #25) +onMounted(() => { + if (route.query.action === 'add') { + // Pre-fill data from query params (from scan) + prefilledData.value = { + barcode: route.query.barcode as string || undefined, + name: route.query.name as string || undefined, + brand: route.query.brand as string || undefined, + image_url: route.query.image_url as string || undefined, + quantity: route.query.quantity as string || undefined, + } + + showAddForm.value = true + + // Clean up URL + router.replace({ query: {} }) + } +}) + +const handleCloseAddForm = () => { + showAddForm.value = false + prefilledData.value = null +} const handleItemAdded = (item: any) => { showAddForm.value = false + prefilledData.value = null // Reload the inventory list inventoryListRef.value?.reload() } diff --git a/app/pages/scan.vue b/app/pages/scan.vue index ae0f8b1..91d304b 100644 --- a/app/pages/scan.vue +++ b/app/pages/scan.vue @@ -72,49 +72,33 @@ definePageMeta({ const scannedBarcode = ref(null) const productData = ref(null) -const isLookingUp = ref(false) -const lookupError = ref(null) const showManualEntry = ref(false) +// Use product lookup composable +const { lookupProduct, isLoading: isLookingUp, error: lookupError } = useProductLookup() + const handleBarcodeDetected = async (barcode: string) => { scannedBarcode.value = barcode - lookupError.value = null - isLookingUp.value = true - - try { - // TODO: Implement product lookup via Edge Function (Issue #24) - // For now, create a basic product object - await new Promise(resolve => setTimeout(resolve, 1000)) // Simulate API call - - productData.value = { - name: `Product ${barcode}`, - brand: 'Unknown Brand', - barcode: barcode, - image_url: null - } - - lookupError.value = 'Product lookup not yet implemented. Using default data.' - } catch (error) { - console.error('Product lookup error:', error) - lookupError.value = 'Failed to look up product. You can still add it manually.' - productData.value = { - name: `Product ${barcode}`, - barcode: barcode - } - } finally { - isLookingUp.value = false + + // Fetch product data from Edge Function + const data = await lookupProduct(barcode) + + if (data) { + productData.value = data } } const addToInventory = () => { - // TODO: Implement scan-to-add flow (Issue #25) - // Navigate to add form with pre-filled data + // Navigate to home page with add form open and pre-filled navigateTo({ path: '/', query: { + action: 'add', barcode: scannedBarcode.value, - name: productData.value?.name, - brand: productData.value?.brand + name: productData.value?.name || undefined, + brand: productData.value?.brand || undefined, + image_url: productData.value?.image_url || undefined, + quantity: productData.value?.quantity || undefined } }) }