import React, { useEffect, useState } from "react"; import { View, Pressable, TextInput, StyleSheet, ActivityIndicator, Switch, Platform, } from "react-native"; import { useColorScheme } from "nativewind"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { ArrowLeft, Calendar, ChevronDown, DollarSign, Send, CalendarSearch, Clock, User, Phone, Building2, Hash, Banknote, Upload, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { api, BASE_URL } from "@/lib/api"; import { toast } from "@/lib/toast-store"; import { useAuthStore } from "@/lib/auth-store"; import * as ImagePicker from "expo-image-picker"; import { PickerModal, SelectOption } from "@/components/PickerModal"; import { CalendarGrid } from "@/components/CalendarGrid"; import { TimerPickerModal } from "react-native-timer-picker"; import { LinearGradient } from "expo-linear-gradient"; import { getPlaceholderColor } from "@/lib/colors"; import { getScanData } from "@/lib/scan-cache"; import { FormFlow } from "@/components/FormFlow"; const S = StyleSheet.create({ input: { paddingHorizontal: 12, paddingVertical: 12, fontSize: 12, fontWeight: "500", borderRadius: 6, borderWidth: 1, textAlignVertical: "center", }, inputCenter: { paddingHorizontal: 12, paddingVertical: 10, fontSize: 14, fontWeight: "500", borderRadius: 6, borderWidth: 1, textAlign: "center", textAlignVertical: "center", }, }); function useInputColors() { const { colorScheme } = useColorScheme(); const dark = colorScheme === "dark"; return { bg: dark ? "rgba(30,30,30,0.8)" : "rgba(241,245,249,0.2)", border: dark ? "rgba(255,255,255,0.08)" : "rgba(203,213,225,0.6)", text: dark ? "#f1f5f9" : "#0f172a", placeholder: "rgba(100,116,139,0.45)", }; } function Field({ label, value, onChangeText, placeholder, numeric = false, center = false, flex, multiline = false, }: { label: string; value: string; onChangeText: (v: string) => void; placeholder: string; numeric?: boolean; center?: boolean; flex?: number; multiline?: boolean; }) { const c = useInputColors(); return ( {label} ); } function Label({ children }: { children: string }) { return ( {children} ); } const currencies = ["ETB", "USD", "EUR", "GBP", "KES", "ZAR"]; const paymentMethods = [ "Telebirr", "CBE", "Dashen", "DECSI", "Bank Transfer", "Cash", "Other", ]; const providers = ["telebirr", "cbe", "dashen", "decsi", "other"]; const STEPS = [ { key: "payment", label: "Payment Details" }, { key: "transaction", label: "Transaction" }, { key: "merchant", label: "Merchant" }, { key: "sender", label: "Sender" }, { key: "verification", label: "Verification" }, { key: "summary", label: "Summary" }, ]; export default function AddReceiptScreen() { const nav = useSirouRouter(); const [step, setStep] = useState(0); const [submitting, setSubmitting] = useState(false); const [scanning, setScanning] = useState(false); const [scanFailures, setScanFailures] = useState(0); const token = useAuthStore((s) => s.token); const [amount, setAmount] = useState(""); const [currency, setCurrency] = useState("ETB"); const [paymentDate, setPaymentDate] = useState( new Date().toISOString().split("T")[0], ); const [paymentTime, setPaymentTime] = useState( new Date().toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false, }), ); const [paymentMethod, setPaymentMethod] = useState("Telebirr"); const [transactionId, setTransactionId] = useState(""); const [referenceNumber, setReferenceNumber] = useState(""); const [merchantName, setMerchantName] = useState(""); const [merchantId, setMerchantId] = useState(""); const [provider, setProvider] = useState("telebirr"); const [senderName, setSenderName] = useState(""); const [senderPhone, setSenderPhone] = useState(""); const [verifyWithProvider, setVerifyWithProvider] = useState(false); const [verifyWithVerifierApi, setVerifyWithVerifierApi] = useState(false); const [showCurrency, setShowCurrency] = useState(false); const [showPaymentMethod, setShowPaymentMethod] = useState(false); const [showProvider, setShowProvider] = useState(false); const [showPaymentDate, setShowPaymentDate] = useState(false); const [showPaymentTime, setShowPaymentTime] = useState(false); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const c = useInputColors(); useEffect(() => { const scanData = getScanData(); if (!scanData) return; if (scanData.amount != null) setAmount(String(scanData.amount)); if (scanData.currency) setCurrency(scanData.currency); if (scanData.paymentDate) { try { setPaymentDate( new Date(scanData.paymentDate).toISOString().split("T")[0], ); } catch (_) {} } if (scanData.paymentTime) setPaymentTime(scanData.paymentTime); if (scanData.paymentMethod) setPaymentMethod(scanData.paymentMethod); if (scanData.transactionId) setTransactionId(scanData.transactionId); if (scanData.referenceNumber) setReferenceNumber(scanData.referenceNumber); if (scanData.merchantName) setMerchantName(scanData.merchantName); if (scanData.merchantId) setMerchantId(scanData.merchantId); if (scanData.provider) setProvider(scanData.provider); if (scanData.senderName) setSenderName(scanData.senderName); if (scanData.senderPhone) setSenderPhone(scanData.senderPhone.replace(/^\+251|\++/g, "")); }, []); const handleNext = () => { if (step === 0) { if (!amount || parseFloat(amount) <= 0) { toast.error( "Validation Error", "Amount is required and must be greater than 0", ); return; } } setStep((s) => s + 1); }; const handleSubmit = async () => { if (!amount || parseFloat(amount) <= 0) { toast.error( "Validation Error", "Amount is required and must be greater than 0", ); throw new Error("Amount is required"); } if (!transactionId) { toast.error("Validation Error", "Transaction ID is required"); throw new Error("Transaction ID is required"); } setSubmitting(true); try { const payload = { amount: parseFloat(amount), currency, paymentDate, paymentTime, paymentMethod, transactionId, referenceNumber, merchantName, merchantId, provider, senderName, senderPhone: senderPhone ? (senderPhone.startsWith("+") ? senderPhone : `+251${senderPhone}`) : undefined, verifyWithProvider, verifyWithVerifierApi, }; await api.scan.paymentReceiptManual({ body: payload }); toast.success("Success", "Receipt added successfully!"); nav.back(); } catch (error: any) { console.error("[AddReceipt] Error:", error); const msg = error?.response?.data?.message || error?.data?.message || error?.message || "Failed to add receipt"; toast.error("Error", msg); throw error; } finally { setSubmitting(false); } }; const handlePickImage = async () => { try { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== "granted") { toast.error( "Permission Denied", "We need access to your gallery to upload receipts.", ); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, quality: 0.8, }); if (!result.canceled && result.assets && result.assets.length > 0) { const uri = result.assets[0].uri; await handleProcessImage(uri); } } catch (e: any) { console.error("[AddReceipt] Pick Image Error:", e); toast.error("Picker Failed", "Could not launch gallery picker."); } }; const handleProcessImage = async (uri: string) => { setScanning(true); toast.info("Processing...", "Uploading receipt to AI extraction engine."); try { const formData = new FormData(); const fileExt = uri.split(".").pop() || "jpg"; const fileName = `receipt-${Date.now()}.${fileExt}`; const type = `image/${fileExt === "jpg" ? "jpeg" : fileExt}`; formData.append("file", { uri: Platform.OS === "android" ? uri : uri.replace("file://", ""), name: fileName, type: type, } as any); const response = await fetch(`${BASE_URL}scan/payment-receipt`, { method: "POST", headers: { Authorization: `Bearer ${token}`, Accept: "application/json", }, body: formData, }); if (!response.ok) { const err = await response .json() .catch(() => ({ message: "Scan processing failed." })); throw new Error(err.message || "AI extraction failed."); } const scanResult = await response.json(); if (!scanResult.success) { throw new Error( scanResult.message || "AI extraction was unsuccessful.", ); } toast.success("Success!", "Data extracted successfully."); const ocr = scanResult.data || {}; if (ocr.amount != null) setAmount(String(ocr.amount)); if (ocr.currency) setCurrency(ocr.currency); if (ocr.paymentDate) { try { setPaymentDate(new Date(ocr.paymentDate).toISOString().split("T")[0]); } catch (_) {} } if (ocr.paymentTime) setPaymentTime(ocr.paymentTime); if (ocr.paymentMethod) setPaymentMethod(ocr.paymentMethod); if (ocr.transactionId) setTransactionId(ocr.transactionId); if (ocr.referenceNumber) setReferenceNumber(ocr.referenceNumber); if (ocr.merchantName) setMerchantName(ocr.merchantName); if (ocr.merchantId) setMerchantId(ocr.merchantId); if (ocr.provider) setProvider(ocr.provider); if (ocr.senderName) setSenderName(ocr.senderName); if (ocr.senderPhone) setSenderPhone(ocr.senderPhone.replace(/^\+251|\++/g, "")); try { await handleSubmit(); } catch { const nextCount = scanFailures + 1; setScanFailures(nextCount); if (nextCount >= 2) { toast.info("Scan failed, fill details below", ""); } else { toast.error("Extraction failed, try again", ""); } } } catch (err: any) { console.error("[AddReceipt] Extraction Error:", err); const nextCount = scanFailures + 1; setScanFailures(nextCount); if (nextCount >= 2) { toast.info("Scan failed, fill details below", ""); } else { toast.error("Extraction failed, try again", ""); } } finally { setScanning(false); } }; const formattedAmount = (parseFloat(amount) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, }); return ( setStep(step - 1)} onComplete={handleSubmit} loading={submitting} completeLabel="Add Receipt" > {step === 0 && ( <> {scanning ? ( ) : ( )} Scan from Gallery Upload image to auto-fill form Currency setShowCurrency(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border }} > {currency} Payment Date setShowPaymentDate(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border }} > {paymentDate} Payment Time setShowPaymentTime(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border }} > {paymentTime || "Select"} Payment Method setShowPaymentMethod(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border }} > {paymentMethod} Provider setShowProvider(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border }} > {provider} )} {step === 1 && ( <> )} {step === 2 && ( <> )} {step === 3 && ( <> Sender Phone +251 )} {step === 4 && ( <> Verify with Provider Check payment status with the provider Verify with Verifier API Run verification through the verifier service )} {step === 5 && ( <> Amount {currency} {formattedAmount} Payment Date {paymentDate} Payment Time {paymentTime} Payment Method {paymentMethod} Provider {provider} Transaction ID {transactionId} {referenceNumber ? ( Reference {referenceNumber} ) : null} {merchantName ? ( Merchant {merchantName} ) : null} {merchantId ? ( Merchant ID {merchantId} ) : null} {senderName ? ( Sender {senderName} ) : null} {senderPhone ? ( Sender Phone +251{senderPhone} ) : null} Total {currency} {formattedAmount} )} setShowCurrency(false)} title="Select Currency" > {currencies.map((curr) => ( { setCurrency(v); setShowCurrency(false); }} /> ))} setShowPaymentMethod(false)} title="Select Payment Method" > {paymentMethods.map((method) => ( { setPaymentMethod(v); setShowPaymentMethod(false); }} /> ))} setShowProvider(false)} title="Select Provider" > {providers.map((prov) => ( { setProvider(v); setShowProvider(false); }} /> ))} setShowPaymentDate(false)} title="Select Payment Date" > { setPaymentDate(v); setShowPaymentDate(false); }} /> setShowPaymentTime(false)} onConfirm={(pickedDuration) => { const hours = String(pickedDuration.hours ?? 0).padStart(2, "0"); const minutes = String(pickedDuration.minutes ?? 0).padStart(2, "0"); setPaymentTime(`${hours}:${minutes}`); setShowPaymentTime(false); }} styles={{ theme: isDark ? "dark" : "light", modalTitle: { fontSize: 18, fontWeight: "700", }, contentContainer: { width: "80%", marginHorizontal: 16, }, confirmButton: { borderRadius: 8, paddingHorizontal: 40, }, cancelButton: { borderRadius: 8, borderWidth: 1, borderColor: "#EDD5D1", paddingHorizontal: 40, }, }} hideSeconds /> ); }