import React, { useState, useCallback } from "react"; import { View, ScrollView, TextInput, ActivityIndicator, Pressable, Linking, Modal, StyleSheet, Alert, Platform, PermissionsAndroid, } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Stack, useLocalSearchParams, useFocusEffect } from "expo-router"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { EmptyState } from "@/components/EmptyState"; import { FormFlow } from "@/components/FormFlow"; import { Wallet, Link2, Clock, AlertTriangle, User, Building2, Hash, Eye, Trash2, Info, Search, ChevronDown, X, Scan, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { api, BASE_URL } from "@/lib/api"; import { useColorScheme } from "nativewind"; import { toast } from "@/lib/toast-store"; import { ActionModal } from "@/components/ActionModal"; import { PickerModal, SelectOption } from "@/components/PickerModal"; import { CalendarGrid } from "@/components/CalendarGrid"; import { getPlaceholderColor } from "@/lib/colors"; let SmsAndroid: any = null; if (Platform.OS === "android") { try { const smsModule = require("react-native-get-sms-android"); SmsAndroid = smsModule.default || smsModule; } catch (e) { console.log("[PaymentDetail] SMS module unavailable"); } } const S = StyleSheet.create({ input: { paddingHorizontal: 12, paddingVertical: 12, fontSize: 12, fontWeight: "500", borderRadius: 6, borderWidth: 1, textAlignVertical: "center", }, }); const CURRENCIES = ["ETB", "USD", "EUR", "GBP", "KES", "ZAR"]; const PAYMENT_METHODS = [ "Telebirr", "CBE", "Dashen", "DECSI", "Bank Transfer", "Cash", "Other", ]; export default function PaymentDetailScreen() { const nav = useSirouRouter(); const { id } = useLocalSearchParams(); const { colorScheme } = useColorScheme(); const isDark = colorScheme === "dark"; const [payment, setPayment] = useState(null); const [loading, setLoading] = useState(true); const [deleting, setDeleting] = useState(false); const [matching, setMatching] = useState(false); const [scanningSms, setScanningSms] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showInvoicePicker, setShowInvoicePicker] = useState(false); const [editSaving, setEditSaving] = useState(false); // Edit form state const [editTxnId, setEditTxnId] = useState(""); const [editAmount, setEditAmount] = useState(""); const [editCurrency, setEditCurrency] = useState("ETB"); const [editPaymentMethod, setEditPaymentMethod] = useState("Telebirr"); const [editPaymentDate, setEditPaymentDate] = useState(""); const [editNotes, setEditNotes] = useState(""); const [editStep, setEditStep] = useState(0); const [showEditCurrency, setShowEditCurrency] = useState(false); const [showEditMethod, setShowEditMethod] = useState(false); const [showEditDate, setShowEditDate] = useState(false); // Invoice list for matching const [invoices, setInvoices] = useState([]); const [invoiceSearch, setInvoiceSearch] = useState(""); const paymentId = Array.isArray(id) ? id[0] : id; function useInputColors() { const dark = isDark; 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)", }; } const c = useInputColors(); const fetchPayment = useCallback(async () => { try { setLoading(true); if (!paymentId) throw new Error("No ID provided"); const response = await api.payments.getById({ params: { id: paymentId }, }); setPayment(response); } catch (error) { console.error("[PaymentDetail] Error fetching payment:", error); toast.error("Error", "Failed to fetch payment details."); } finally { setLoading(false); } }, [paymentId]); useFocusEffect( useCallback(() => { fetchPayment(); }, [fetchPayment]), ); const handleDelete = () => setShowDeleteModal(true); const confirmDelete = async () => { setDeleting(true); try { if (!paymentId) return; await api.payments.delete({ params: { id: paymentId } }); toast.success("Deleted", "Payment record has been removed."); setShowDeleteModal(false); nav.back(); } catch (err: any) { toast.error("Error", err.message || "Failed to delete payment."); setShowDeleteModal(false); } finally { setDeleting(false); } }; const handleScanSms = async () => { if (Platform.OS !== "android") { toast.error( "Not Supported", "SMS scanning is only available on Android.", ); return; } setScanningSms(true); try { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.READ_SMS, ); if (granted !== PermissionsAndroid.RESULTS.GRANTED) { toast.error( "Permission Denied", "SMS access is required to scan payment messages.", ); setScanningSms(false); return; } if (!SmsAndroid) { toast.error("Not Available", "SMS scanning module is not available."); setScanningSms(false); return; } const fiveMinsAgo = Date.now() - 5 * 60 * 1000; const filter = { box: "inbox", minDate: fiveMinsAgo, maxCount: 30 }; SmsAndroid.list( JSON.stringify(filter), (fail: string) => { toast.error("Scan Failed", fail); setScanningSms(false); }, (_count: number, smsList: string) => { const messages = JSON.parse(smsList); const match = messages.find((m: any) => { const addr = (m.address || "").toUpperCase(); const body = (m.body || "").toUpperCase(); return ( addr === "127" || addr === "CBE" || body.includes("TELEBIRR") || body.includes("CBE") ); }); if (!match) { toast.error( "No Match", "No CBE or Telebirr SMS found in the last 5 minutes.", ); setScanningSms(false); return; } const text = match.body; const lines = [ `From: ${match.address}`, `Date: ${new Date(match.date).toLocaleString()}`, "", `"${text}"`, "", "Send this SMS to verify the payment?", ]; Alert.alert("SMS Found", lines.filter(Boolean).join("\n"), [ { text: "Cancel", style: "cancel", onPress: () => setScanningSms(false), }, { text: "Verify", onPress: async () => { try { await api.payments.verifySms({ params: { id: paymentId }, body: { smsContent: text }, }); toast.success( "Verified", "SMS has been sent for verification.", ); fetchPayment(); } catch (err: any) { toast.error("Error", err.message || "Verification failed."); } finally { setScanningSms(false); } }, }, ]); }, ); } catch (err) { toast.error("Error", "Something went wrong during SMS scan."); setScanningSms(false); } }; const openEdit = () => { const amt = Number( typeof payment.amount === "object" ? payment.amount.value : payment.amount, ); setEditTxnId(payment.transactionId || ""); setEditAmount(String(amt)); setEditCurrency(payment.currency || "ETB"); setEditPaymentMethod(payment.paymentMethod || "Telebirr"); setEditPaymentDate( payment.paymentDate ? new Date(payment.paymentDate).toISOString().split("T")[0] : new Date().toISOString().split("T")[0], ); setEditNotes(payment.notes || ""); setEditStep(0); setShowEditModal(true); }; const handleEditSave = async () => { if (!editAmount || parseFloat(editAmount) <= 0) { toast.error("Validation Error", "Amount must be greater than 0"); return; } setEditSaving(true); try { await api.payments.update({ params: { id: paymentId }, body: { transactionId: editTxnId, amount: parseFloat(editAmount), currency: editCurrency, paymentDate: new Date(editPaymentDate).toISOString(), paymentMethod: editPaymentMethod, notes: editNotes, }, }); toast.success("Success", "Payment updated successfully."); setShowEditModal(false); fetchPayment(); } catch (err: any) { const msg = err?.response?.data?.message || err?.data?.message || err?.message || "Failed to update payment"; toast.error("Error", msg); } finally { setEditSaving(false); } }; const openMatchPicker = async () => { if (!payment || matching || !paymentId) return; setMatching(true); try { const response = await api.invoices.getAll(); const list = Array.isArray(response) ? response : (response as any).data || []; setInvoices(list); setInvoiceSearch(""); setShowInvoicePicker(true); } catch (err: any) { toast.error("Error", "Failed to fetch invoices."); } finally { setMatching(false); } }; const handleInvoiceSelect = async (inv: any) => { setShowInvoicePicker(false); try { await api.payments.associate({ params: { id: paymentId }, body: { invoiceId: inv.id }, }); toast.success( "Success", `Payment associated with invoice #${inv.invoiceNumber || inv.id}.`, ); fetchPayment(); } catch (err: any) { toast.error("Error", err.message || "Failed to associate."); } }; if (loading) { return ( Retrieving Transaction... ); } if (!payment) { return ( Transaction Not Found The requested payment record could not be retrieved. ); } const amountValue = Number( typeof payment.amount === "object" ? payment.amount.value : payment.amount, ); const paymentDate = payment.paymentDate ? new Date(payment.paymentDate) : new Date(payment.createdAt); const isFlagged = payment.isFlagged === true; const filteredInvoices = invoices.filter((inv) => { if (!invoiceSearch) return true; const q = invoiceSearch.toLowerCase(); return ( (inv.invoiceNumber || "").toLowerCase().includes(q) || (inv.customerName || "").toLowerCase().includes(q) ); }); return ( {/* Flagged Alert */} {isFlagged && ( Security Flag ({payment.flagReason || "Audit Needed"}) {payment.flagNotes || "System flagged this for manual review."} )} {/* Hero Section */} Total Amount {amountValue.toLocaleString()} {payment.currency || "USD"} Sender {payment.senderName || "Unknown"} Method {payment.paymentMethod || "Direct"} {/* Details Section */} Transaction Details Transaction ID {payment.transactionId || "—"} Receiver {payment.receiverName || "—"} Payment Date {paymentDate.toLocaleString()} {payment.invoiceId && ( nav.go("invoices/[id]", { id: payment.invoiceId }) } className="flex-row items-center gap-3 active:opacity-60" > Linked Invoice {payment.invoiceId} )} Created {new Date(payment.createdAt).toLocaleString()} {/* Notes */} {payment.notes && ( Notes "{payment.notes}" )} {/* Actions */} {payment.receiptPath && ( )} {!payment.invoiceId && ( )} {/* Edit Modal */} setShowEditModal(false)} > Edit Payment setShowEditModal(false)} className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" > setEditStep(editStep + 1)} onBack={() => setEditStep(editStep - 1)} onComplete={handleEditSave} loading={editSaving} completeLabel="Save Changes" hideHeader > {editStep === 0 && ( Payment Details Transaction ID Amount Currency setShowEditCurrency(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border, }} > {editCurrency} )} {editStep === 1 && ( Schedule Payment Date setShowEditDate(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border }} > {editPaymentDate || "Select Date"} Method setShowEditMethod(true)} className="h-11 px-3 border border-border rounded-[6px] flex-row items-center justify-between" style={{ backgroundColor: c.bg, borderColor: c.border }} > {editPaymentMethod} )} {editStep === 2 && ( Notes Notes )} setShowEditCurrency(false)} title="Select Currency" > {CURRENCIES.map((curr) => ( { setEditCurrency(v); setShowEditCurrency(false); }} /> ))} setShowEditMethod(false)} title="Select Payment Method" > {PAYMENT_METHODS.map((method) => ( { setEditPaymentMethod(v); setShowEditMethod(false); }} /> ))} setShowEditDate(false)} title="Select Payment Date" > { setEditPaymentDate(v); setShowEditDate(false); }} /> {/* Invoice Picker Modal */} setShowInvoicePicker(false)} title="Select Invoice" > {filteredInvoices.length > 0 ? ( filteredInvoices.map((inv) => ( handleInvoiceSelect(inv)} className="px-4 py-3 border-b border-border/40 flex-row items-center" > {inv.customerName || "Unknown"} #{inv.invoiceNumber || inv.id} · {inv.currency || "ETB"}{" "} {Number(inv.amount || 0).toLocaleString()} )) ) : ( )} setShowDeleteModal(false)} onConfirm={confirmDelete} title="Delete Payment" description="Are you sure you want to permanently delete this payment record? This action cannot be reversed." confirmText="Delete" confirmVariant="destructive" icon={Trash2} iconColor="#ef4444" loading={deleting} /> ); }