'use client'; import { useState, useEffect } from 'react'; import { ArrowLeft, FileText, CheckCircle2, AlertCircle, Loader2, DollarSign, Calendar, Building2, TrendingUp, X, } from 'lucide-react'; import Link from 'next/link'; import { formatCurrency, formatDate } from '@/lib/utils'; import { apiFetch, apiJson, ApiError } from '@/lib/api-client'; interface InvoiceLine { id: string; lineNumber: number; description: string; quantity: number; unitPrice: number; lineAmount: number; glAccountCode: string; taxAmount: number; } interface Payment { id: string; paymentNumber: string; paymentDate: string; paymentAmount: number; paymentMethod: string; status: string; } interface Customer { id: string; customerName: string; customerCode: string; } interface Invoice { id: string; invoiceNumber: string; invoiceType: string; invoiceDate: string; dueDate: string; status: string; vendor: null; customer: Customer; subtotalAmount: number; taxAmount: number; totalAmount: number; paidAmount: number; balanceDue: number; currencyCode: string; invoiceLines: InvoiceLine[]; payments: Payment[]; } const getStatusBadgeColor = (status: string): { bg: string; text: string } => { switch (status) { case 'DRAFT': return { bg: 'bg-[#1A1A1F]', text: 'text-[#F0F0F3]' }; case 'PENDING_APPROVAL': return { bg: 'bg-yellow-100', text: 'text-yellow-800' }; case 'APPROVED': return { bg: 'bg-blue-900/25', text: 'text-blue-800' }; case 'PAID': return { bg: 'bg-green-900/25', text: 'text-green-800' }; case 'OVERDUE': return { bg: 'bg-red-900/25', text: 'text-red-800' }; case 'PARTIALLY_PAID': return { bg: 'bg-blue-900/25', text: 'text-blue-800' }; case 'CANCELLED': return { bg: 'bg-[#1A1A1F]', text: 'text-[#F0F0F3]' }; case 'VOID': return { bg: 'bg-[#1A1A1F]', text: 'text-[#F0F0F3]' }; default: return { bg: 'bg-[#1A1A1F]', text: 'text-[#F0F0F3]' }; } }; const calculateDaysDifference = (date1: string, date2: string): number => { const d1 = new Date(date1); const d2 = new Date(date2); const diffTime = Math.abs(d2.getTime() - d1.getTime()); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; }; export default function ARInvoiceDetailPage({ params }: { params: { invoiceId: string } }) { const [invoice, setInvoice] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showPaymentModal, setShowPaymentModal] = useState(false); const [paymentLoading, setPaymentLoading] = useState(false); const [paymentError, setPaymentError] = useState(null); const [formData, setFormData] = useState({ paymentAmount: '', paymentDate: new Date().toISOString().split('T')[0], paymentMethod: 'ACH', }); useEffect(() => { const fetchInvoice = async () => { try { setLoading(true); setError(null); const data = await apiJson(`/api/invoices/${params.invoiceId}`, { silent: true }); setInvoice(data); // Check if query params indicate we should open payment modal const queryParams = new URLSearchParams(window.location.search); if (queryParams.get('action') === 'payment') { setShowPaymentModal(true); } } catch (err) { if (err instanceof ApiError && err.status === 404) { setError('Invoice not found'); } else { setError(err instanceof ApiError ? err.message : 'Error fetching invoice data'); } setInvoice(null); } finally { setLoading(false); } }; fetchInvoice(); }, [params.invoiceId]); const handleAction = async (action: string) => { try { const updatedInvoice = await apiJson(`/api/invoices/${params.invoiceId}`, { method: 'POST', body: JSON.stringify({ action }), }); setInvoice(updatedInvoice); } catch (err) { console.error(`Error performing action: ${action}`, err); } }; const handleRecordPayment = async (e: React.FormEvent) => { e.preventDefault(); if (!invoice) return; setPaymentLoading(true); setPaymentError(null); try { const amount = parseFloat(formData.paymentAmount); if (isNaN(amount) || amount <= 0) { setPaymentError('Payment amount must be greater than 0'); setPaymentLoading(false); return; } await apiFetch('/api/payments', { method: 'POST', body: JSON.stringify({ paymentDate: formData.paymentDate, paymentMethod: formData.paymentMethod, paymentAmount: amount, customerId: invoice.customer.id, invoiceId: invoice.id, paymentAllocations: [ { invoiceId: invoice.id, allocatedAmount: amount, }, ], }), silent: true, }); // Success - close modal and refresh invoice setShowPaymentModal(false); setFormData({ paymentAmount: '', paymentDate: new Date().toISOString().split('T')[0], paymentMethod: 'ACH', }); // Refetch the invoice to show updated payment status try { const updatedInvoice = await apiJson(`/api/invoices/${params.invoiceId}`, { silent: true }); setInvoice(updatedInvoice); } catch { // If refetch fails, just refresh the page window.location.reload(); } } catch (err) { setPaymentError( err instanceof ApiError ? err.message : err instanceof Error ? err.message : 'An error occurred while recording payment' ); } finally { setPaymentLoading(false); } }; const handleModalClose = () => { setShowPaymentModal(false); setPaymentError(null); setFormData({ paymentAmount: '', paymentDate: new Date().toISOString().split('T')[0], paymentMethod: 'ACH', }); }; if (loading) { return (

Loading invoice...

); } if (error || !invoice) { return (
Back to AR Invoices

{error || 'Invoice Not Found'}

The invoice you are looking for could not be found.

); } const statusColor = getStatusBadgeColor(invoice.status); const termsDays = calculateDaysDifference(invoice.invoiceDate, invoice.dueDate); return (
{/* Back Button */} Back to AR Invoices {/* Header */}

{invoice.invoiceNumber}

Customer:{' '} {invoice.customer.customerName}

{invoice.status}
{/* Summary Section */}

Invoice Date

{formatDate(invoice.invoiceDate)}

Due Date

{formatDate(invoice.dueDate)}

Terms

Net {termsDays}

Currency

{invoice.currencyCode}

Customer Code

{invoice.customer.customerCode}

{/* Invoice Line Items */}

Line Items

{invoice.invoiceLines.map((line) => ( ))}
Line Description Qty Unit Price Amount Tax GL Account
{line.lineNumber} {line.description} {line.quantity} {formatCurrency(line.unitPrice)} {formatCurrency(line.lineAmount)} {formatCurrency(line.taxAmount)} {line.glAccountCode}

Subtotal

Tax

Received

Balance Due

{formatCurrency(invoice.subtotalAmount)}

{formatCurrency(invoice.taxAmount)}

{formatCurrency(invoice.paidAmount)}

{formatCurrency(invoice.balanceDue)}

{/* Amount Summary Cards */}

Total Amount

{formatCurrency(invoice.totalAmount)}

Amount Received

{formatCurrency(invoice.paidAmount)}

Balance Due

{formatCurrency(invoice.balanceDue)}

Tax Amount

{formatCurrency(invoice.taxAmount)}

{/* Actions */}

Actions

{/* Payment History */} {invoice.payments && invoice.payments.length > 0 && (

Payment Received

{invoice.payments.map((payment) => ( ))}
Payment # Date Amount Method Status
{payment.paymentNumber} {formatDate(payment.paymentDate)} {formatCurrency(payment.paymentAmount)} {payment.paymentMethod} {payment.status}
)} {/* Revenue Recognition Schedule */}

Revenue Recognition Schedule

This invoice contains revenue recognition based on service delivery and ASC 606 standards.

{invoice.invoiceLines.map((line) => (

{line.description}

GL Account: {line.glAccountCode}

{formatCurrency(line.lineAmount)}

Recognized on: {formatDate(invoice.invoiceDate)}
))}
{/* Generated Journal Entries */}

Generated Journal Entries

Invoice Posted

JE-{invoice.id.substring(0, 6).toUpperCase()}

Debit: Accounts Receivable | Credit: Revenue Accounts

{formatCurrency(invoice.totalAmount)}

{/* Customer Portal Activity */}

Customer Portal Activity

Invoice Sent

{formatDate(invoice.invoiceDate)} | Customer notification sent

{invoice.payments && invoice.payments.length > 0 && ( <>

Payment Received

{formatDate(invoice.payments[0].paymentDate)} | Amount:{' '} {formatCurrency(invoice.payments[0].paymentAmount)}

Payment Confirmed

{formatDate(invoice.payments[0].paymentDate)} | Funds received and applied to account

)}
{/* Attached Documents */}

Attached Documents

{invoice.invoiceNumber}.pdf

Invoice Document

Download

Service Agreement - {invoice.customer.customerCode}.pdf

Service Agreement

Download
{/* Audit Trail */}

Audit Trail

{invoice.payments && invoice.payments.length > 0 && ( )}
Date/Time User Action Details
{formatDate(invoice.payments[0].paymentDate)} 11:45 AM System Payment Applied {invoice.payments[0].paymentNumber} fully applied to invoice
{formatDate(invoice.invoiceDate)} 01:00 PM System Invoice Posted Journal entry recorded and invoice sent
{formatDate(invoice.invoiceDate)} 12:30 PM System Created Invoice created and posted
{/* Payment Recording Modal */} {showPaymentModal && (

Record Payment

{paymentError && (
{paymentError}
)}
$ setFormData((prev) => ({ ...prev, paymentAmount: e.target.value, })) } className="w-full pl-8 pr-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-[#F0F0F3]" />

Balance due: {formatCurrency(invoice.balanceDue)}

setFormData((prev) => ({ ...prev, paymentDate: e.target.value, })) } className="w-full px-3 py-2 border border-[#3A3A45] rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-[#F0F0F3]" />
)}
); }