initial commit

This commit is contained in:
Josh Myers
2026-04-09 20:36:10 -07:00
commit 4681b1a3c8
248 changed files with 97032 additions and 0 deletions

View File

@@ -0,0 +1,747 @@
'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<Invoice | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [paymentLoading, setPaymentLoading] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(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<any>(`/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<any>(`/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<any>(`/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 (
<div className="min-h-screen bg-[#111114] p-6 flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<Loader2 size={40} className="text-blue-600 animate-spin" />
<p className="text-[#8B8B9E] text-lg">Loading invoice...</p>
</div>
</div>
);
}
if (error || !invoice) {
return (
<div className="min-h-screen bg-[#111114] p-6">
<Link
href="/finance/ar"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 mb-6"
>
<ArrowLeft size={20} />
Back to AR Invoices
</Link>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-8 flex flex-col items-center gap-4">
<AlertCircle size={48} className="text-red-600" />
<h2 className="text-2xl font-bold text-[#F0F0F3]">{error || 'Invoice Not Found'}</h2>
<p className="text-[#8B8B9E]">The invoice you are looking for could not be found.</p>
</div>
</div>
);
}
const statusColor = getStatusBadgeColor(invoice.status);
const termsDays = calculateDaysDifference(invoice.invoiceDate, invoice.dueDate);
return (
<div className="min-h-screen bg-[#111114] p-6">
{/* Back Button */}
<Link
href="/finance/ar"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 mb-6"
>
<ArrowLeft size={20} />
Back to AR Invoices
</Link>
{/* Header */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6 mb-6">
<div className="flex justify-between items-start gap-4">
<div>
<h1 className="text-3xl font-bold text-[#F0F0F3]">{invoice.invoiceNumber}</h1>
<p className="text-lg text-[#8B8B9E] mt-2">
Customer:{' '}
<span className="text-blue-600 font-semibold hover:underline cursor-pointer">
{invoice.customer.customerName}
</span>
</p>
</div>
<span className={`px-4 py-2 ${statusColor.bg} ${statusColor.text} font-semibold rounded-full text-sm`}>
{invoice.status}
</span>
</div>
</div>
{/* Summary Section */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium flex items-center gap-2">
<Calendar size={16} />
Invoice Date
</p>
<p className="text-lg font-semibold text-[#F0F0F3] mt-1">{formatDate(invoice.invoiceDate)}</p>
</div>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium flex items-center gap-2">
<Calendar size={16} />
Due Date
</p>
<p className="text-lg font-semibold text-[#F0F0F3] mt-1">{formatDate(invoice.dueDate)}</p>
</div>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium">Terms</p>
<p className="text-lg font-semibold text-[#F0F0F3] mt-1">Net {termsDays}</p>
</div>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium flex items-center gap-2">
<DollarSign size={16} />
Currency
</p>
<p className="text-lg font-semibold text-[#F0F0F3] mt-1">{invoice.currencyCode}</p>
</div>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium flex items-center gap-2">
<Building2 size={16} />
Customer Code
</p>
<p className="text-lg font-semibold text-[#F0F0F3] mt-1">{invoice.customer.customerCode}</p>
</div>
</div>
{/* Invoice Line Items */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm overflow-hidden mb-6">
<div className="p-6 border-b border-[#2A2A32]">
<h2 className="text-xl font-bold text-[#F0F0F3]">Line Items</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-[#111114] border-b border-[#2A2A32]">
<tr>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#8B8B9E]">Line</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#8B8B9E]">Description</th>
<th className="px-6 py-3 text-right text-sm font-semibold text-[#8B8B9E]">Qty</th>
<th className="px-6 py-3 text-right text-sm font-semibold text-[#8B8B9E]">Unit Price</th>
<th className="px-6 py-3 text-right text-sm font-semibold text-[#8B8B9E]">Amount</th>
<th className="px-6 py-3 text-right text-sm font-semibold text-[#8B8B9E]">Tax</th>
<th className="px-6 py-3 text-left text-sm font-semibold text-[#8B8B9E]">GL Account</th>
</tr>
</thead>
<tbody className="divide-y divide-[#2A2A32]">
{invoice.invoiceLines.map((line) => (
<tr key={line.id} className="hover:bg-[#111114]">
<td className="px-6 py-4 text-[#F0F0F3]">{line.lineNumber}</td>
<td className="px-6 py-4 text-[#F0F0F3]">{line.description}</td>
<td className="px-6 py-4 text-right text-[#F0F0F3]">{line.quantity}</td>
<td className="px-6 py-4 text-right text-[#F0F0F3]">
{formatCurrency(line.unitPrice)}
</td>
<td className="px-6 py-4 text-right font-semibold text-[#F0F0F3]">
{formatCurrency(line.lineAmount)}
</td>
<td className="px-6 py-4 text-right text-[#F0F0F3]">
{formatCurrency(line.taxAmount)}
</td>
<td className="px-6 py-4">
<Link
href={`/finance/general-ledger/account/${line.glAccountCode}`}
className="text-blue-600 hover:text-blue-800 hover:underline font-medium"
>
{line.glAccountCode}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="bg-[#111114] px-6 py-4 border-t border-[#2A2A32]">
<div className="flex justify-end gap-32">
<div>
<p className="text-[#8B8B9E] text-sm">Subtotal</p>
<p className="text-[#8B8B9E] text-sm mt-2">Tax</p>
<p className="text-[#8B8B9E] text-sm mt-2">Received</p>
<p className="text-xl font-bold text-[#F0F0F3] mt-2">Balance Due</p>
</div>
<div className="text-right">
<p className="text-[#F0F0F3] font-semibold">{formatCurrency(invoice.subtotalAmount)}</p>
<p className="text-[#F0F0F3] font-semibold mt-2">{formatCurrency(invoice.taxAmount)}</p>
<p className="text-[#F0F0F3] font-semibold mt-2">{formatCurrency(invoice.paidAmount)}</p>
<p className="text-2xl font-bold text-[#F0F0F3] mt-2">
{formatCurrency(invoice.balanceDue)}
</p>
</div>
</div>
</div>
</div>
{/* Amount Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium">Total Amount</p>
<p className="text-lg font-semibold text-[#F0F0F3] mt-1">
{formatCurrency(invoice.totalAmount)}
</p>
</div>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium">Amount Received</p>
<p className="text-lg font-semibold text-green-600 mt-1">
{formatCurrency(invoice.paidAmount)}
</p>
</div>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium">Balance Due</p>
<p className="text-lg font-semibold text-red-600 mt-1">
{formatCurrency(invoice.balanceDue)}
</p>
</div>
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-4">
<p className="text-[#8B8B9E] text-sm font-medium">Tax Amount</p>
<p className="text-lg font-semibold text-[#F0F0F3] mt-1">
{formatCurrency(invoice.taxAmount)}
</p>
</div>
</div>
{/* Actions */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6 mb-6">
<h2 className="text-xl font-bold text-[#F0F0F3] mb-4">Actions</h2>
<div className="flex flex-wrap gap-3">
<button
onClick={() => handleAction('SEND')}
className="px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors"
>
Send
</button>
<button
onClick={() => setShowPaymentModal(true)}
className="px-4 py-2 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition-colors"
>
Record Payment
</button>
<button
onClick={() => handleAction('CANCEL')}
className="px-4 py-2 bg-orange-600 text-white font-semibold rounded-lg hover:bg-orange-700 transition-colors"
>
Cancel
</button>
<button
onClick={() => handleAction('VOID')}
className="px-4 py-2 bg-red-600 text-white font-semibold rounded-lg hover:bg-red-700 transition-colors"
>
Void
</button>
</div>
</div>
{/* Payment History */}
{invoice.payments && invoice.payments.length > 0 && (
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6 mb-6">
<h2 className="text-xl font-bold text-[#F0F0F3] mb-6">Payment Received</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-[#111114] border-b border-[#2A2A32]">
<tr>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">Payment #</th>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">Date</th>
<th className="px-6 py-3 text-right font-semibold text-[#8B8B9E]">Amount</th>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">Method</th>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-[#2A2A32]">
{invoice.payments.map((payment) => (
<tr key={payment.id} className="hover:bg-[#111114]">
<td className="px-6 py-3 text-[#F0F0F3] font-medium">{payment.paymentNumber}</td>
<td className="px-6 py-3 text-[#F0F0F3]">{formatDate(payment.paymentDate)}</td>
<td className="px-6 py-3 text-right font-semibold text-[#F0F0F3]">
{formatCurrency(payment.paymentAmount)}
</td>
<td className="px-6 py-3 text-[#F0F0F3]">{payment.paymentMethod}</td>
<td className="px-6 py-3">
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-900/25 text-green-800 font-semibold rounded text-xs">
<CheckCircle2 size={14} />
{payment.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Revenue Recognition Schedule */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6 mb-6">
<h2 className="text-xl font-bold text-[#F0F0F3] mb-4 flex items-center gap-2">
<TrendingUp size={24} />
Revenue Recognition Schedule
</h2>
<p className="text-[#8B8B9E] text-sm mb-6">
This invoice contains revenue recognition based on service delivery and ASC 606 standards.
</p>
<div className="space-y-4">
{invoice.invoiceLines.map((line) => (
<div key={line.id} className="border border-[#2A2A32] rounded-lg p-4">
<div className="flex justify-between items-start mb-3">
<div>
<p className="font-semibold text-[#F0F0F3]">{line.description}</p>
<p className="text-sm text-[#8B8B9E]">GL Account: {line.glAccountCode}</p>
</div>
<p className="text-lg font-bold text-[#F0F0F3]">
{formatCurrency(line.lineAmount)}
</p>
</div>
<div className="bg-blue-900/15 px-3 py-2 rounded text-sm text-blue-800">
Recognized on: {formatDate(invoice.invoiceDate)}
</div>
</div>
))}
</div>
</div>
{/* Generated Journal Entries */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6 mb-6">
<h2 className="text-xl font-bold text-[#F0F0F3] mb-6">Generated Journal Entries</h2>
<div className="space-y-4">
<div className="border border-[#2A2A32] rounded-lg p-4">
<div className="flex justify-between items-start">
<div>
<p className="text-sm text-[#8B8B9E] font-medium">Invoice Posted</p>
<p className="text-blue-600 hover:underline font-bold text-lg mt-1 cursor-pointer">
JE-{invoice.id.substring(0, 6).toUpperCase()}
</p>
<p className="text-sm text-[#8B8B9E] mt-2">Debit: Accounts Receivable | Credit: Revenue Accounts</p>
</div>
<p className="text-lg font-semibold text-[#F0F0F3]">
{formatCurrency(invoice.totalAmount)}
</p>
</div>
</div>
</div>
</div>
{/* Customer Portal Activity */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6 mb-6">
<h2 className="text-xl font-bold text-[#F0F0F3] mb-6">Customer Portal Activity</h2>
<div className="space-y-4">
<div className="flex items-start gap-4 p-4 border border-[#2A2A32] rounded-lg">
<CheckCircle2 size={20} className="text-blue-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<p className="font-semibold text-[#F0F0F3]">Invoice Sent</p>
<p className="text-sm text-[#8B8B9E]">{formatDate(invoice.invoiceDate)} | Customer notification sent</p>
</div>
</div>
{invoice.payments && invoice.payments.length > 0 && (
<>
<div className="flex items-start gap-4 p-4 border border-[#2A2A32] rounded-lg">
<CheckCircle2 size={20} className="text-green-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<p className="font-semibold text-[#F0F0F3]">Payment Received</p>
<p className="text-sm text-[#8B8B9E]">
{formatDate(invoice.payments[0].paymentDate)} | Amount:{' '}
{formatCurrency(invoice.payments[0].paymentAmount)}
</p>
</div>
</div>
<div className="flex items-start gap-4 p-4 border border-[#2A2A32] rounded-lg">
<CheckCircle2 size={20} className="text-green-600 flex-shrink-0 mt-1" />
<div className="flex-1">
<p className="font-semibold text-[#F0F0F3]">Payment Confirmed</p>
<p className="text-sm text-[#8B8B9E]">
{formatDate(invoice.payments[0].paymentDate)} | Funds received and applied to account
</p>
</div>
</div>
</>
)}
</div>
</div>
{/* Attached Documents */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6 mb-6">
<h2 className="text-xl font-bold text-[#F0F0F3] mb-6">Attached Documents</h2>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 border border-[#2A2A32] rounded-lg hover:bg-[#111114]">
<FileText size={20} className="text-[#8B8B9E]" />
<div className="flex-1">
<p className="font-semibold text-[#F0F0F3]">{invoice.invoiceNumber}.pdf</p>
<p className="text-sm text-[#8B8B9E]">Invoice Document</p>
</div>
<a href="#" className="text-blue-600 hover:text-blue-800 text-sm font-semibold">
Download
</a>
</div>
<div className="flex items-center gap-3 p-3 border border-[#2A2A32] rounded-lg hover:bg-[#111114]">
<FileText size={20} className="text-[#8B8B9E]" />
<div className="flex-1">
<p className="font-semibold text-[#F0F0F3]">Service Agreement - {invoice.customer.customerCode}.pdf</p>
<p className="text-sm text-[#8B8B9E]">Service Agreement</p>
</div>
<a href="#" className="text-blue-600 hover:text-blue-800 text-sm font-semibold">
Download
</a>
</div>
</div>
</div>
{/* Audit Trail */}
<div className="bg-[#1A1A1F] rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-[#F0F0F3] mb-6">Audit Trail</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-[#111114] border-b border-[#2A2A32]">
<tr>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">Date/Time</th>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">User</th>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">Action</th>
<th className="px-6 py-3 text-left font-semibold text-[#8B8B9E]">Details</th>
</tr>
</thead>
<tbody className="divide-y divide-[#2A2A32]">
{invoice.payments && invoice.payments.length > 0 && (
<tr className="hover:bg-[#111114]">
<td className="px-6 py-3 text-[#F0F0F3]">{formatDate(invoice.payments[0].paymentDate)} 11:45 AM</td>
<td className="px-6 py-3 text-[#F0F0F3]">System</td>
<td className="px-6 py-3 text-[#F0F0F3]">Payment Applied</td>
<td className="px-6 py-3 text-[#8B8B9E]">
{invoice.payments[0].paymentNumber} fully applied to invoice
</td>
</tr>
)}
<tr className="hover:bg-[#111114]">
<td className="px-6 py-3 text-[#F0F0F3]">{formatDate(invoice.invoiceDate)} 01:00 PM</td>
<td className="px-6 py-3 text-[#F0F0F3]">System</td>
<td className="px-6 py-3 text-[#F0F0F3]">Invoice Posted</td>
<td className="px-6 py-3 text-[#8B8B9E]">Journal entry recorded and invoice sent</td>
</tr>
<tr className="hover:bg-[#111114]">
<td className="px-6 py-3 text-[#F0F0F3]">{formatDate(invoice.invoiceDate)} 12:30 PM</td>
<td className="px-6 py-3 text-[#F0F0F3]">System</td>
<td className="px-6 py-3 text-[#F0F0F3]">Created</td>
<td className="px-6 py-3 text-[#8B8B9E]">Invoice created and posted</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Payment Recording Modal */}
{showPaymentModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-[#1A1A1F] rounded-lg shadow-lg max-w-md w-full">
<div className="flex justify-between items-center p-6 border-b border-[#2A2A32]">
<h3 className="text-lg font-bold text-[#F0F0F3]">Record Payment</h3>
<button
onClick={handleModalClose}
className="text-[#5A5A6E] hover:text-[#8B8B9E]"
>
<X size={24} />
</button>
</div>
<form onSubmit={handleRecordPayment} className="p-6 space-y-4">
{paymentError && (
<div className="bg-red-900/15 border border-red-200 text-red-800 px-4 py-3 rounded-lg text-sm">
{paymentError}
</div>
)}
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-2">
Payment Amount
</label>
<div className="relative">
<span className="absolute left-3 top-2.5 text-[#5A5A6E]">$</span>
<input
type="number"
step="0.01"
min="0"
placeholder={formatCurrency(invoice.balanceDue).replace('$', '')}
value={formData.paymentAmount}
onChange={(e) =>
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]"
/>
</div>
<p className="text-xs text-[#5A5A6E] mt-1">
Balance due: {formatCurrency(invoice.balanceDue)}
</p>
</div>
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-2">
Payment Date
</label>
<input
type="date"
value={formData.paymentDate}
onChange={(e) =>
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]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[#8B8B9E] mb-2">
Payment Method
</label>
<select
value={formData.paymentMethod}
onChange={(e) =>
setFormData((prev) => ({
...prev,
paymentMethod: 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]"
>
<option value="ACH">ACH</option>
<option value="WIRE">Wire Transfer</option>
<option value="CHECK">Check</option>
<option value="CREDIT_CARD">Credit Card</option>
<option value="BANK_TRANSFER">Bank Transfer</option>
<option value="CASH">Cash</option>
<option value="OTHER">Other</option>
</select>
</div>
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={handleModalClose}
className="flex-1 px-4 py-2 bg-gray-200 text-[#F0F0F3] font-semibold rounded-lg hover:bg-gray-300 transition-colors disabled:opacity-50"
disabled={paymentLoading}
>
Cancel
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
disabled={paymentLoading}
>
{paymentLoading && <Loader2 size={16} className="animate-spin" />}
{paymentLoading ? 'Recording...' : 'Record Payment'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}