import React, { useState, useEffect, useRef } from "react"; import { View, Pressable, Platform, ActivityIndicator, Alert, Image, StyleSheet, } from "react-native"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { X, Zap, Camera as CameraIcon, ScanLine, Check, RefreshCw, } 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 [scanType, setScanType] = useState<"invoice" | "receipt">("invoice"); const [scanning, setScanning] = useState(false); const [previewUri, setPreviewUri] = useState(null); 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 handleCapture = async () => { if (!cameraRef.current || scanning) return; try { const photo = await cameraRef.current.takePictureAsync({ quality: 0.8, base64: false, }); if (photo?.uri) { setPreviewUri(photo.uri); } } catch (err) { console.error("[Scan] Capture Error:", err); toast.error("Capture Failed", "Could not take a photo."); } }; const handleProcess = async () => { if (!previewUri || scanning) return; setScanning(true); try { const label = scanType === "invoice" ? "invoice" : "receipt"; toast.info("Processing...", `Uploading ${label} image for AI extraction.`); // Build multipart form data with the image file (binary) const formData = new FormData(); const fileExt = previewUri.split(".").pop() || "jpg"; const fileName = `${label}-${Date.now()}.${fileExt}`; const type = `image/${fileExt === "jpg" ? "jpeg" : fileExt}`; // In React Native, the file object in FormData needs special treatment formData.append("file", { uri: Platform.OS === "android" ? previewUri : previewUri.replace("file://", ""), name: fileName, type: type, } as any); const endpoint = scanType === "invoice" ? "scan/invoice" : "scan/payment-receipt"; const response = await fetch(`${BASE_URL}${endpoint}`, { method: "POST", headers: { Authorization: `Bearer ${token}`, Accept: "application/json", // Boundary is set automatically by fetch when body is FormData }, body: formData, }); if (!response.ok) { const err = await response.json().catch(() => ({ message: "Scan processing failed." })); throw new Error(err.message || "Extraction failed."); } const scanResult = await response.json(); console.log(`[Scan] Extracted ${label} data:`, scanResult); if (!scanResult.success) { throw new Error(scanResult.message || "AI extraction was unsuccessful."); } toast.success("Success!", `Extracted data from ${label} successfully.`); // 4. Map OCR data to structure const ocr = scanResult.data || {}; const invoicePayload = { invoiceNumber: ocr.invoiceNumber || `DOC-${Date.now()}`, customerName: ocr.customerName?.trim() || "Unknown Entity", customerEmail: ocr.customerEmail || "", customerPhone: ocr.customerPhone || "", amount: ocr.totalAmount || ocr.subtotalAmount || 0, currency: ocr.currency || "ETB", type: scanType === "invoice" ? "SALES" : "EXPENSE", 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 ${scanType === "invoice" ? "Invoice" : "Receipt"} #${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 record in the backend const createResponse = await api.invoices.create({ body: invoicePayload, headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); console.log("[Scan] Record created successfully:", createResponse); if (createResponse?.id) { nav.go(`invoices/${createResponse.id}`); } else { nav.go("(tabs)/payments"); } } catch (err: any) { console.error("[Scan] Processing Error:", err); toast.error("Processing Failed", err.message || "Document extraction failed."); } 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 ( {previewUri ? ( Preview setPreviewUri(null)} className="h-12 w-12 rounded-full bg-black/40 items-center justify-center border border-white/20" > ) : ( {/* 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 */} {/* Capture Button & Tabs */} {/* Tabs */} setScanType("invoice")} className={`px-6 py-2 rounded-xl ${scanType === "invoice" ? "bg-primary" : "bg-transparent"}`} > Invoice setScanType("receipt")} className={`px-6 py-2 rounded-xl ${scanType === "receipt" ? "bg-primary" : "bg-transparent"}`} > Receipt {"Tap to Capture"} )} ); }