748 lines
30 KiB
TypeScript
748 lines
30 KiB
TypeScript
'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>
|
|
);
|
|
}
|