import React, { useState, useCallback } from "react"; import { View, ScrollView, ActivityIndicator, Linking, useColorScheme, Pressable, Modal, Dimensions, } 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 { Calendar, Share2, Download, Trash2, Package, Clock, User, Hash, AlertCircle, Mail, MessageSquare, X, MoreVertical, FileText, Receipt, CreditCard, ChevronRight, Check, Edit, } from "@/lib/icons"; import { ScreenWrapper } from "@/components/ScreenWrapper"; 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"; import { UploadIcon } from "lucide-react-native"; const { height: SCREEN_HEIGHT } = Dimensions.get("window"); 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 [showDeleteModal, setShowDeleteModal] = useState(false); const [showShareSheet, setShowShareSheet] = useState(false); const [showMoreSheet, setShowMoreSheet] = useState(false); const [sharing, setSharing] = useState(false); const [activeTab, setActiveTab] = useState<"details" | "activity">("details"); useFocusEffect( useCallback(() => { 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 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 handleShare = async (channel: "email" | "sms") => { try { setSharing(true); const invoiceId = Array.isArray(id) ? id[0] : id; await api.invoices.shareLink({ body: { invoiceId, channel }, }); toast.success( "Sent", `Invoice shared via ${channel === "email" ? "email" : "SMS"}`, ); setShowShareSheet(false); } catch (err: any) { toast.error("Error", err?.message || "Failed to share invoice"); } finally { setSharing(false); } }; const handleDelete = () => 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 discountPercent = subtotalValue > 0 ? Math.round((discountValue / subtotalValue) * 100) : 0; const status = (invoice.status || "PENDING").toUpperCase(); const isPaid = status === "PAID"; const statusLabel: Record = { PAID: "Paid", PENDING: "Pending", DRAFT: "Draft", CANCELLED: "Cancelled", }; const customerName = ( invoice.customerName?.replace("Customer Name: ", "") || "Walking Client" ).trim(); const paymentDate = invoice.dueDate ? new Date(invoice.dueDate) : invoice.issueDate ? new Date(invoice.issueDate) : new Date(invoice.createdAt); const formatLongDate = (d: Date) => d.toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric", }); 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 */} $ {Number(amountValue).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} No.{" "} {invoice.invoiceNumber || `INV${(invoice.id || "").slice(0, 8).toUpperCase()}`} Status {isPaid ? ( ) : ( )} {statusLabel[status] || "Pending"} {isPaid ? "Payment Date" : "Due Date"} {isPaid ? `Paid at ${formatLongDate(paymentDate)}` : formatLongDate(paymentDate)} {/* Tabs */} setActiveTab("details")} className="pb-2.5" > Details {activeTab === "details" && ( )} setActiveTab("activity")} className="pb-2.5" > Activity Log {activeTab === "activity" && ( )} {activeTab === "details" ? ( Billed to {invoice.customerEmail || "—"} Billing details {customerName} Invoice Number {invoice.invoiceNumber || `INV${(invoice.id || "").slice(0, 8).toUpperCase()}`} Notes {invoice.notes || "-"} {items.length > 0 ? ( Product {items.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, })}{" "} {invoice.currency || "USD"} {lineTotal.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} ); })} ) : null} Subtotal {subtotalValue.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} {discountValue > 0 ? ( Discount {discountPercent > 0 ? `${discountPercent}%` : ""} -{" "} {discountValue.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} ) : null} {taxAmountValue > 0 ? ( Tax +{" "} {taxAmountValue.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} ) : null} Total {amountValue.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} Amount due {amountValue.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} ) : ( {items.length > 0 ? ( Items {items.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, })}{" "} {invoice.currency || "USD"} {lineTotal.toLocaleString("en-US", { minimumFractionDigits: 2, })}{" "} {invoice.currency || "USD"} ); })} ) : ( No activity yet Events related to this invoice will show up here. )} )} setShowShareSheet(true)} className="flex-1 h-12 rounded-[8px] border border-border items-center justify-center flex-row gap-2 bg-card" > Send Invoice Export setShowShareSheet(false)} > setShowShareSheet(false)} > e.stopPropagation()} > Share setShowShareSheet(false)} className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" > } label="Send as Email" description="Public accessible shortened link via yaltopia.com" onPress={() => handleShare("email")} loading={sharing} /> } label="Send as SMS" description="Public accessible shortened link via yaltopia.com" onPress={() => handleShare("sms")} loading={sharing} /> setShowMoreSheet(false)} > setShowMoreSheet(false)} > e.stopPropagation()} > Invoice setShowMoreSheet(false)} className="h-8 w-8 bg-secondary/80 rounded-full items-center justify-center border border-border/10" > } label="Edit Invoice" description="Update details, items, or dates" onPress={() => { setShowMoreSheet(false); nav.go("invoices/edit", { id: invoice.id }); }} /> } label="Delete Invoice" description="Permanently remove this record" onPress={() => { setShowMoreSheet(false); setShowDeleteModal(true); }} danger /> 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} /> ); } function ActionOption({ icon, label, description, onPress, danger, loading, }: { icon: React.ReactNode; label: string; description: string; onPress?: () => void; danger?: boolean; loading?: boolean; }) { return ( {loading ? : icon} {label} {description} ); }