import React, { useState, useEffect } from "react"; import { View, ScrollView, ActivityIndicator, Alert, Linking, useColorScheme, Pressable, Platform, PermissionsAndroid, } from "react-native"; import { useSirouRouter } from "@sirou/react-native"; import { AppRoutes } from "@/lib/routes"; import { Stack, useLocalSearchParams } from "expo-router"; import { Text } from "@/components/ui/text"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { FileText, Calendar, Share2, Download, Trash2, Package, Clock, ExternalLink, ChevronRight, User, CreditCard, Hash, AlertCircle, MessageSquare, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; import { ShadowWrapper } from "@/components/ShadowWrapper"; import { StandardHeader } from "@/components/StandardHeader"; import { api, BASE_URL } from "@/lib/api"; import { toast } from "@/lib/toast-store"; import { useAuthStore } from "@/lib/auth-store"; import { ActionModal } from "@/components/ActionModal"; // Android only SMS module 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("[InvoiceDetail] SMS module unavailable"); } } export default function InvoiceDetailScreen() { const nav = useSirouRouter(); const { id } = useLocalSearchParams(); const colorScheme = useColorScheme(); const isDark = colorScheme === "dark"; const [loading, setLoading] = useState(true); const [invoice, setInvoice] = useState(null); const [scanningSms, setScanningSms] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); useEffect(() => { fetchInvoice(); }, [id]); const fetchInvoice = async () => { try { setLoading(true); const invoiceId = Array.isArray(id) ? id[0] : id; if (!invoiceId) throw new Error("No ID provided"); const data = await api.invoices.getById({ params: { id: invoiceId } }); setInvoice(data); } catch (error: any) { console.error("[InvoiceDetail] Error:", error); toast.error("Error", "Failed to load invoice details"); } finally { setLoading(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", "We need SMS access to verify payments.", ); setScanningSms(false); return; } toast.info( "Scanning SMS", "Searching for bank messages from the last 30 minutes...", ); // Simulate logic if native module is missing (Expo Go) if (!SmsAndroid) { setTimeout(() => { toast.error( "No Match", "No matching banking SMS found in the last 30 minutes.", ); setScanningSms(false); }, 2000); return; } const thirtyMinsAgo = Date.now() - 30 * 60 * 1000; const filter = { box: "inbox", minDate: thirtyMinsAgo, maxCount: 20, }; SmsAndroid.list( JSON.stringify(filter), (fail: string) => { toast.error("Scan Failed", fail); setScanningSms(false); }, (count: number, smsList: string) => { const messages = JSON.parse(smsList); const amountStr = amountValue.toString(); const custName = (invoice.customerName || "").toUpperCase(); // Search for amount or customer name in SMS body const match = messages.find((m: any) => { const body = m.body.toUpperCase(); return ( body.includes(amountStr) || (custName && body.includes(custName)) ); }); if (match) { Alert.alert( "Payment Found!", `We found a matching SMS proof for ${amountValue} ${invoice.currency}. Would you like to attach this to the invoice?`, [ { text: "No", style: "cancel" }, { text: "Attach SMS", onPress: () => toast.success( "Attached", "SMS proof linked to invoice successfully.", ), }, ], ); } else { toast.error( "No Match", "Could not find any matching banking SMS in the last 30 minutes.", ); } setScanningSms(false); }, ); } catch (err) { toast.error("Error", "Something went wrong during SMS scan."); setScanningSms(false); } }; const handleGetPdf = async () => { try { const { token } = useAuthStore.getState(); const pdfUrl = `${BASE_URL}invoices/${id}/pdf?token=${token}`; await Linking.openURL(pdfUrl); } catch (error) { console.error("[InvoiceDetail] PDF Error:", error); toast.error("Error", "Failed to open PDF"); } }; const handleDelete = async () => { setShowDeleteModal(true); }; const confirmDelete = async () => { try { setLoading(true); const invoiceId = Array.isArray(id) ? id[0] : id; await api.invoices.delete({ params: { id: invoiceId as string }, }); toast.success("Success", "Invoice deleted successfully"); setShowDeleteModal(false); nav.back(); } catch (error) { console.error("[InvoiceDetail] Delete Error:", error); toast.error("Error", "Failed to delete invoice"); setShowDeleteModal(false); setLoading(false); } }; if (loading) { return ( ); } if (!invoice) { return ( Invoice Not Found The requested document could not be retrieved. ); } // Robust data extraction const originalData = invoice.scannedData?.originalData || {}; const items = (invoice.items?.length > 0 ? invoice.items : originalData.items) || []; const taxAmountValue = Number( typeof invoice.taxAmount === "object" ? invoice.taxAmount?.value : invoice.taxAmount || originalData.taxAmount || 0, ); const discountValue = Number( typeof invoice.discountAmount === "object" ? invoice.discountAmount?.value : invoice.discountAmount || originalData.discountAmount || 0, ); let amountValue = Number( typeof invoice.amount === "object" ? invoice.amount.value : invoice.amount, ); if (items.length > 0) { const itemsTotal = items.reduce( (acc: number, item: any) => acc + (Number(item.total?.value || item.total) || 0), 0, ); if ( itemsTotal > 0 && (amountValue === taxAmountValue || amountValue < itemsTotal) ) { amountValue = itemsTotal + taxAmountValue - discountValue; } } const subtotalValue = amountValue - taxAmountValue + discountValue; const statusColors = { PAID: { bg: "bg-emerald-500/10", text: "text-emerald-500", dot: "bg-emerald-500", }, PENDING: { bg: "bg-amber-500/10", text: "text-amber-500", dot: "bg-amber-500", }, DRAFT: { bg: "bg-blue-500/10", text: "text-blue-500", dot: "bg-blue-500" }, DEFAULT: { bg: "bg-slate-500/10", text: "text-slate-500", dot: "bg-slate-500", }, }; const status = (invoice.status || "PENDING").toUpperCase(); const colors = statusColors[status as keyof typeof statusColors] || statusColors.DEFAULT; return ( nav.go("invoices/edit", { id: invoice.id })} /> Total Amount {Number(amountValue).toLocaleString(undefined, { minimumFractionDigits: 2, })} {invoice.currency || "ETB"} Date {new Date( invoice.issueDate || invoice.createdAt, ).toLocaleDateString()} Due {new Date(invoice.dueDate).toLocaleDateString()} Client {invoice.customerName?.replace("Customer Name: ", "") || "Walking Client"} Items {items.length === 0 ? ( No items found ) : ( {items.map((item: any, idx: number) => ( {item.description} {Number( item.total?.value || item.total || 0, ).toLocaleString()} {item.quantity} x{" "} {Number( item.unitPrice?.value || item.unitPrice || 0, ).toLocaleString()}{" "} {invoice.currency} ))} )} Subtotal {subtotalValue.toLocaleString()} {invoice.currency} Grand Total {amountValue.toLocaleString()} {invoice.currency} setShowDeleteModal(false)} onConfirm={confirmDelete} title="Delete Invoice" description="Are you sure you want to delete this invoice? This will remove all associated data and cannot be recovered." confirmText="Delete" confirmVariant="destructive" icon={Trash2} iconColor="#ef4444" loading={loading} /> ); }