import React, { useState, useEffect } from "react"; import { View, ScrollView, ActivityIndicator, Alert, Linking, useColorScheme, Pressable, } 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, } 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"; 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); useEffect(() => { fetchInvoice(); }, [id]); const fetchInvoice = async () => { try { setLoading(true); // Ensure id is a string if useLocalSearchParams returns an array 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 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 () => { Alert.alert( "Delete Invoice", "Are you sure you want to delete this invoice? This action cannot be undone.", [ { text: "Cancel", style: "cancel" }, { text: "Delete", style: "destructive", onPress: 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"); nav.back(); } catch (error) { console.error("[InvoiceDetail] Delete Error:", error); toast.error("Error", "Failed to delete invoice"); setLoading(false); } }, }, ], ); }; if (loading) { return ( ); } if (!invoice) { return ( Invoice Not Found The requested document could not be retrieved. ); } // Robust data extraction with fallback for scanned invoices 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, ); // Intelligence: If amount looks like it's just the tax, and we have items, use items total 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 })} /> {/* Modern Hero Area */} {status} Total Payable Amount {Number(amountValue).toLocaleString(undefined, { minimumFractionDigits: 2, })} {invoice.currency || "ETB"} {/* Quick Stats Grid */} Issue Date {new Date( invoice.issueDate || invoice.createdAt, ).toLocaleDateString()} Due Date {new Date(invoice.dueDate).toLocaleDateString()} {/* Client Box */} Billed To {invoice.customerName?.replace("Customer Name: ", "") || "Walking Client"} {invoice.customerEmail && ( {invoice.customerEmail} )} #{invoice.id.split("-")[0]} {/* Detailed Items Table */} Order Summary {items.map((item: any, idx: number) => ( {item.description} {Number( item.total?.value || item.total || 0, ).toLocaleString()} {item.quantity} units x{" "} {Number( item.unitPrice?.value || item.unitPrice || 0, ).toLocaleString()}{" "} {invoice.currency} ))} {items.length === 0 && ( No line items specified )} {/* Billing Breakdown */} Subtotal {subtotalValue.toLocaleString()} {invoice.currency} {taxAmountValue > 0 && ( Tax (extracted) +{taxAmountValue.toLocaleString()} {invoice.currency} )} {discountValue > 0 && ( Discount -{discountValue.toLocaleString()} {invoice.currency} )} Grand Total Verified from data {amountValue.toLocaleString()} {invoice.currency} {/* Notes */} {invoice.notes && ( Note / Description " {invoice.notes} " )} {/* Premium Actions */} ); }