import React, { useState, useCallback, useEffect } from "react"; import { View, ScrollView, TextInput, ActivityIndicator, Pressable, Linking, Modal, StyleSheet, Alert, Platform, PermissionsAndroid, Dimensions, Image, } from "react-native"; import { WebView } from "react-native-webview"; 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, ChevronRight, X, Scan, MoreVertical, Edit, Package, Share2, Mail, MessageSquare, Calendar, Check, Camera, ArrowUpRight, Flag, } 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"; import ticketImage from "@/assets/ticket.png"; import { CheckCircle2, CreditCardIcon } from "lucide-react-native"; 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 { height: SCREEN_HEIGHT } = Dimensions.get("window"); 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); const [showMoreSheet, setShowMoreSheet] = useState(false); const [activeTab, setActiveTab] = useState<"details" | "items" | "image">( "details", ); const [linkedInvoice, setLinkedInvoice] = useState(null); const [showImageFullScreen, setShowImageFullScreen] = useState(false); const [imageLoading, setImageLoading] = useState(false); const [showFlagModal, setShowFlagModal] = useState(false); const [flagReason, setFlagReason] = useState<"FAKE" | "SCAM" | "OTHER">( "FAKE", ); const [flagNotes, setFlagNotes] = useState(""); const [flagging, setFlagging] = 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]), ); useEffect(() => { const fetchLinked = async () => { if (!payment?.invoiceId) { setLinkedInvoice(null); return; } try { const inv = await api.invoices.getById({ params: { id: payment.invoiceId }, }); setLinkedInvoice(inv); } catch (err) { console.log("[PaymentDetail] could not load linked invoice", err); setLinkedInvoice(null); } }; fetchLinked(); }, [payment?.invoiceId]); 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."); } }; const openFlagModal = () => { if (!payment) return; setFlagReason(payment.flagReason || "FAKE"); setFlagNotes(payment.flagNotes || ""); setShowMoreSheet(false); setShowFlagModal(true); }; const handleLinkInvoicePress = () => { if (payment?.invoiceId) { toast.warning( "Already Linked", "This payment is already linked to an invoice.", ); return; } openMatchPicker(); }; const handleFlag = async () => { if (!paymentId) return; setFlagging(true); try { await api.payments.flag({ params: { id: paymentId }, body: { flagReason, flagNotes }, }); toast.success("Flagged", "Payment has been flagged for review."); setShowFlagModal(false); fetchPayment(); } catch (err: any) { const msg = err?.response?.data?.message || err?.data?.message || err?.message || "Failed to flag payment"; toast.error("Error", msg); } finally { setFlagging(false); } }; 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 scannedImageRaw = payment.scannedData?.imageUrl || payment.scannedData?.image || payment.scannedData?.imagePath || payment.scannedData?.originalData?.imageUrl || payment.imageUrl || payment.imagePath || payment.receiptPath || null; const scannedImageUrl = scannedImageRaw ? scannedImageRaw.startsWith("http") ? scannedImageRaw : `${BASE_URL}${scannedImageRaw.replace(/^\//, "")}` : null; const hasScannedImage = Boolean(payment?.isScanned && scannedImageUrl); const hasSms = Boolean( payment.smsContent || payment.smsVerified || payment.smsBody || payment.verifiedAt || payment.smsId, ); const linkedItems = (linkedInvoice?.items?.length > 0 ? linkedInvoice.items : linkedInvoice?.scannedData?.originalData?.items) || []; const filteredInvoices = invoices.filter((inv) => { if (!invoiceSearch) return true; const q = invoiceSearch.toLowerCase(); return ( (inv.invoiceNumber || "").toLowerCase().includes(q) || (inv.customerName || "").toLowerCase().includes(q) ); }); const formatLongDate = (d: Date) => d.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric", }); const ActionOption = ({ icon, label, description, onPress, destructive, }: { icon: React.ReactNode; label: string; description: string; onPress: () => void; destructive?: boolean; }) => ( {icon} {label} {description} ); return ( setShowMoreSheet(true)} className="h-10 w-10 rounded-[10px] bg-card items-center justify-center border border-border" > } /> {/* Hero Card — illustration overflows the top */} {amountValue.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, })}{" "} {payment.currency || "USD"} {payment.transactionId ? `Txn ${payment.transactionId}` : payment.paymentMethod || "Direct"} Method {payment.financialInstitutionLogoUrl ? ( ) : ( )} {payment.financialInstitution || payment.paymentMethod || "Direct"} Sender {payment.senderName || "Unknown"} Date {formatLongDate(paymentDate)} {isFlagged && ( Flagged {payment.flagReason || "Audit Needed"} )} {payment.isReferenceVerified && ( Reference Verified Yes )} {/* Tabs */} setActiveTab("details")} className="pb-2.5" > Details {activeTab === "details" && ( )} {hasScannedImage && ( setActiveTab("image")} className="pb-2.5" > Image {activeTab === "image" && ( )} )} {/* Tab content */} {activeTab === "details" ? ( {/* Transaction Details */} Transaction ID {payment.transactionId || "—"} Payment Method {payment.paymentMethod || "—"} {payment.financialInstitutionLogoUrl ? ( ) : ( )} Financial Institution {payment.financialInstitution || "—"} Sender {payment.senderName || "—"} Receiver {payment.receiverName || "—"} Payment Date {paymentDate.toLocaleString()} Reference {payment.isReferenceVerified ? "Verified" : "Not verified"} {isFlagged && ( Flagged for review {payment.flagReason ? ( Reason {payment.flagReason} ) : null} {payment.flagNotes ? ( {payment.flagNotes} ) : null} )} {payment.invoiceId ? ( nav.go("invoices/[id]", { id: payment.invoiceId }) } className="flex-row items-center gap-3 active:opacity-60" > Linked Invoice {payment.invoiceId} ) : ( Linked Invoice {matching ? ( ) : ( <> Link Invoice )} )} Created {new Date(payment.createdAt).toLocaleString()} {/* Note */} {payment.notes && ( Note {payment.notes} )} ) : activeTab === "image" ? ( {hasScannedImage ? ( Scanned Document setShowImageFullScreen(true)} className="rounded-[10px] overflow-hidden border border-border bg-card active:opacity-80" > setImageLoading(true)} onLoadEnd={() => setImageLoading(false)} onError={() => { setImageLoading(false); toast.error( "Image Error", "Failed to load scanned image.", ); }} renderError={() => ( Failed to load image )} /> {imageLoading && ( )} Tap to view full screen ) : ( No image available The scanned image could not be found. )} ) : ( {linkedItems.length > 0 ? ( Items {linkedItems.map((item: any, idx: number) => { const qty = Number( item.quantity?.value || item.quantity || 1, ); const unitPrice = Number( item.unitPrice?.value || item.unitPrice || 0, ); const lineTotal = Number( item.total?.value || item.total || qty * unitPrice, ); return ( {item.description || `Item ${idx + 1}`} {qty} ×{" "} {unitPrice.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {payment.currency || "USD"} {lineTotal.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {payment.currency || "USD"} ); })} ) : payment.invoiceId ? ( Loading items from linked invoice... ) : ( No items linked Link an invoice to see its items here. {!payment.invoiceId && ( )} )} )} {/* Action buttons */} {payment.receiptPath && ( Linking.openURL( `${BASE_URL}${payment.receiptPath.replace(/^\//, "")}`, ) } className="h-12 rounded-[8px] border border-border items-center justify-center flex-row gap-2 bg-card" > View Receipt )} {/* Sticky bottom bar — Scan SMS + Link Invoice side by side (like invoice detail) */} {scanningSms ? ( ) : ( <> {hasSms ? "SMS Verified" : "Scan SMS"} )} {matching ? ( ) : ( <> {payment.invoiceId ? "Linked" : "Link Invoice"} )} {/* More bottom sheet */} setShowMoreSheet(false)} > setShowMoreSheet(false)} > e.stopPropagation()} > Payment setShowMoreSheet(false)} className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" > } label="Edit Payment" description="Update amount, method, or dates" onPress={() => { setShowMoreSheet(false); openEdit(); }} /> } label={isFlagged ? "Update Flag" : "Flag Payment"} description="Mark this payment for manual review" onPress={openFlagModal} /> } label="Delete Payment" description="Permanently remove this record" onPress={() => { setShowMoreSheet(false); handleDelete(); }} destructive /> {/* 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} /> {/* Flag Modal */} setShowFlagModal(false)} > setShowFlagModal(false)} > e.stopPropagation()} className="bg-card rounded-t-[36px] overflow-hidden border-t-[3px] border-border/20" style={{ maxHeight: SCREEN_HEIGHT * 0.8 }} > Flag Payment setShowFlagModal(false)} className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" > Mark this payment as suspicious. The reason will be visible to your team and recorded in the audit log. Reason {(["FAKE", "SCAM", "OTHER"] as const).map((r) => ( setFlagReason(r)} className={`h-11 px-3 border rounded-[6px] flex-row items-center justify-between ${ flagReason === r ? "border-primary bg-primary/10" : "border-border" }`} style={{ backgroundColor: flagReason === r ? isDark ? "rgba(234,88,12,0.15)" : "rgba(234,88,12,0.08)" : c.bg, }} > {r === "FAKE" ? "Fake Payment" : r === "SCAM" ? "Scam / Phishing" : "Other"} {flagReason === r && ( )} ))} Notes (optional) {flagging ? ( ) : ( <> {isFlagged ? "Update Flag" : "Submit Flag"} )} {/* Full screen image viewer */} setShowImageFullScreen(false)} > setShowImageFullScreen(false)} > {scannedImageUrl && ( )} setShowImageFullScreen(false)} className="absolute top-12 right-5 h-10 w-10 rounded-full bg-black/60 items-center justify-center border border-white/20" > ); }