import React, { useState, useEffect, useRef } from "react"; import { View, Pressable, Platform, ActivityIndicator, Alert, } from "react-native"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { X, Zap, Camera as CameraIcon, ScanLine } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { CameraView, useCameraPermissions } from "expo-camera"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { useNavigation } from "expo-router"; import { api, BASE_URL } from "@/lib/api"; import { useAuthStore } from "@/lib/auth-store"; import { toast } from "@/lib/toast-store"; const NAV_BG = "#ffffff"; export default function ScanScreen() { const nav = useSirouRouter(); const [permission, requestPermission] = useCameraPermissions(); const [torch, setTorch] = useState(false); const [scanning, setScanning] = useState(false); const cameraRef = useRef(null); const navigation = useNavigation(); const token = useAuthStore((s) => s.token); useEffect(() => { navigation.setOptions({ tabBarStyle: { display: "none" } }); return () => { navigation.setOptions({ tabBarStyle: { display: "flex", backgroundColor: NAV_BG, borderTopWidth: 0, elevation: 10, height: 75, paddingBottom: Platform.OS === "ios" ? 30 : 10, paddingTop: 10, marginHorizontal: 20, position: "absolute", bottom: 25, left: 20, right: 20, borderRadius: 32, shadowColor: "#000", shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.12, shadowRadius: 20, }, }); }; }, [navigation]); const handleScan = async () => { if (!cameraRef.current || scanning) return; setScanning(true); try { // 1. Capture the photo const photo = await cameraRef.current.takePictureAsync({ quality: 0.85, base64: false, }); if (!photo?.uri) throw new Error("Failed to capture photo."); toast.info("Scanning...", "Uploading invoice image for AI extraction."); // 2. Build multipart form data with the image file const formData = new FormData(); formData.append("file", { uri: photo.uri, name: "invoice.jpg", type: "image/jpeg", } as any); // 3. POST to /api/v1/scan/invoice const response = await fetch(`${BASE_URL}scan/invoice`, { method: "POST", headers: { Authorization: `Bearer ${token}`, // Do NOT set Content-Type here — fetch sets it automatically with the boundary for multipart }, body: formData, }); if (!response.ok) { const err = await response.json(); throw new Error(err.message || "Scan failed."); } const scanResult = await response.json(); console.log("[Scan] Extracted invoice data:", scanResult); if (!scanResult.success) { throw new Error(scanResult.message || "Extraction failed."); } toast.success("Scan Complete!", "Drafting your invoice now..."); // 4. Map OCR data to Invoice structure const ocr = scanResult.data || {}; const invoicePayload = { invoiceNumber: ocr.invoiceNumber || `INV-${Date.now()}`, customerName: ocr.customerName?.trim() || "Unknown Customer", customerEmail: ocr.customerEmail || "", customerPhone: ocr.customerPhone || "", amount: ocr.totalAmount || ocr.subtotalAmount || 0, currency: ocr.currency || "ETB", type: "SALES", status: "DRAFT", issueDate: ocr.issueDate ? new Date(ocr.issueDate).toISOString() : new Date().toISOString(), dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), description: `Scanned Invoice #${ocr.invoiceNumber || ""}`, notes: scanResult.message || "Automatically generated from scan.", taxAmount: ocr.taxAmount || 0, discountAmount: 0, isScanned: true, scannedData: { sellerTIN: ocr.sellerTIN || "", items: ocr.items || [], }, items: (ocr.items || []).map((item: any) => ({ description: typeof item === "string" ? item : item.description || "Item", quantity: item.quantity || 1, unitPrice: item.unitPrice || item.total || 0, total: item.total || 0, })), }; // 5. Create the invoice in the backend const createResponse = await api.invoices.create({ body: invoicePayload, }); console.log("[Scan] Invoice created successfully:", createResponse); toast.success("Success!", "Invoice created and ready for review."); // 6. Navigate to the new invoice detail page if (createResponse?.id) { nav.go(`invoices/${createResponse.id}`); } else { nav.go("(tabs)/payments"); } } catch (err: any) { console.error("[Scan] Error:", err); toast.error( "Scan Failed", err.message || "Could not process the invoice.", ); } finally { setScanning(false); } }; if (!permission) { return ; } if (!permission.granted) { return ( Camera Access We need your permission to use the camera to scan invoices and receipts automatically. nav.back()} className="mt-6"> Go Back ); } return ( {/* Top bar */} setTorch(!torch)} className={`h-12 w-12 rounded-full items-center justify-center border border-white/20 ${torch ? "bg-primary" : "bg-black/40"}`} > nav.back()} className="h-12 w-12 rounded-full bg-black/40 items-center justify-center border border-white/20" > {/* Scan Frame */} Align Invoice Within Frame {/* Capture Button */} {scanning ? ( ) : ( )} {scanning ? "Extracting Data..." : "Tap to Scan"} ); }