273 lines
13 KiB
TypeScript
273 lines
13 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
|
|
interface Invoice {
|
|
invoice_id: number;
|
|
invoice_number: string;
|
|
customer_id: string;
|
|
invoice_date: string;
|
|
due_date: string;
|
|
subtotal: number;
|
|
tax_amount: number;
|
|
total_amount: number;
|
|
status: string;
|
|
payment_terms: string | null;
|
|
notes: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export default function PrintInvoice() {
|
|
const [invoiceList, setInvoiceList] = useState<Invoice[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);
|
|
|
|
useEffect(() => {
|
|
const getInvoiceListData = async () => {
|
|
try {
|
|
const response = await fetch('http://localhost:2058/host-api/v1/invoice/invoice-info/', {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.message || 'Session fetch failed');
|
|
}
|
|
|
|
setInvoiceList(data.data);
|
|
} catch (error: any) {
|
|
console.error('Fetch error:', error);
|
|
setError(error.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
getInvoiceListData();
|
|
}, []);
|
|
|
|
const handlePrint = () => {
|
|
window.print();
|
|
}
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD'
|
|
}).format(amount);
|
|
}
|
|
|
|
const viewInvoiceDetails = (invoice: Invoice) => {
|
|
setSelectedInvoice(invoice);
|
|
}
|
|
|
|
const closeInvoiceDetails = () => {
|
|
setSelectedInvoice(null);
|
|
}
|
|
|
|
if (error) {
|
|
return <div className="p-4 text-red-600">Error: {error}</div>;
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="p-4">Loading invoice data...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="p-4 max-w-6xl mx-auto">
|
|
{/* Invoice List */}
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-bold mb-4">Invoices</h1>
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full bg-white border border-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Invoice #</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Due Date</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{invoiceList.map((invoice) => (
|
|
<tr key={invoice.invoice_id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">{invoice.invoice_number}</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.invoice_date)}</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">{formatDate(invoice.due_date)}</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">{formatCurrency(invoice.total_amount)}</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
|
${invoice.status === 'paid' ? 'bg-green-100 text-green-800' :
|
|
invoice.status === 'draft' ? 'bg-yellow-100 text-yellow-800' :
|
|
'bg-red-100 text-red-800'}`}>
|
|
{invoice.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<button
|
|
onClick={() => viewInvoiceDetails(invoice)}
|
|
className="text-blue-600 hover:text-blue-900 mr-2"
|
|
>
|
|
View
|
|
</button>
|
|
<button
|
|
onClick={handlePrint}
|
|
className="text-gray-600 hover:text-gray-900"
|
|
>
|
|
Print
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Invoice Detail Modal */}
|
|
{selectedInvoice && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
|
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
|
{/* Printable Invoice */}
|
|
<div id="printable-invoice" className="p-8">
|
|
<div className="flex justify-between items-start mb-8">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-800">INVOICE</h1>
|
|
<p className="text-gray-600">{selectedInvoice.invoice_number}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-gray-600">Status: <span className={`font-semibold
|
|
${selectedInvoice.status === 'paid' ? 'text-green-600' :
|
|
selectedInvoice.status === 'draft' ? 'text-yellow-600' :
|
|
'text-red-600'}`}>
|
|
{selectedInvoice.status.toUpperCase()}
|
|
</span></p>
|
|
<p className="text-gray-600">Date: {formatDate(selectedInvoice.invoice_date)}</p>
|
|
<p className="text-gray-600">Due: {formatDate(selectedInvoice.due_date)}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-8">
|
|
<h2 className="text-xl font-semibold mb-2">From</h2>
|
|
<p className="text-gray-800">Your Company Name</p>
|
|
<p className="text-gray-600">123 Business Street</p>
|
|
<p className="text-gray-600">City, State 12345</p>
|
|
<p className="text-gray-600">contact@yourcompany.com</p>
|
|
</div>
|
|
|
|
<div className="mb-8">
|
|
<h2 className="text-xl font-semibold mb-2">Bill To</h2>
|
|
<p className="text-gray-800">Customer ID: {selectedInvoice.customer_id}</p>
|
|
<p className="text-gray-600">[Customer Name]</p>
|
|
<p className="text-gray-600">[Customer Address]</p>
|
|
<p className="text-gray-600">[Customer Email]</p>
|
|
</div>
|
|
|
|
<div className="border-t border-b border-gray-200 py-4 mb-6">
|
|
<div className="grid grid-cols-12 gap-4 font-semibold text-gray-700">
|
|
<div className="col-span-6">Description</div>
|
|
<div className="col-span-2 text-right">Subtotal</div>
|
|
<div className="col-span-2 text-right">Tax</div>
|
|
<div className="col-span-2 text-right">Total</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
<div className="grid grid-cols-12 gap-4 mb-2">
|
|
<div className="col-span-6 text-gray-800">[Service/Product Description]</div>
|
|
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.subtotal)}</div>
|
|
<div className="col-span-2 text-right">{formatCurrency(selectedInvoice.tax_amount)}</div>
|
|
<div className="col-span-2 text-right font-semibold">{formatCurrency(selectedInvoice.total_amount)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end mb-8">
|
|
<div className="w-64">
|
|
<div className="flex justify-between py-2 border-b border-gray-200">
|
|
<span className="font-semibold">Subtotal:</span>
|
|
<span>{formatCurrency(selectedInvoice.subtotal)}</span>
|
|
</div>
|
|
<div className="flex justify-between py-2 border-b border-gray-200">
|
|
<span className="font-semibold">Tax:</span>
|
|
<span>{formatCurrency(selectedInvoice.tax_amount)}</span>
|
|
</div>
|
|
<div className="flex justify-between py-2 font-bold text-lg">
|
|
<span>Total:</span>
|
|
<span>{formatCurrency(selectedInvoice.total_amount)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedInvoice.notes && (
|
|
<div className="mb-8">
|
|
<h2 className="text-xl font-semibold mb-2">Notes</h2>
|
|
<p className="text-gray-600">{selectedInvoice.notes}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="pt-8 border-t border-gray-200">
|
|
<p className="text-gray-600 text-center">Thank you for your business!</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 border-t border-gray-200 flex justify-end">
|
|
<button
|
|
onClick={closeInvoiceDetails}
|
|
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 mr-2"
|
|
>
|
|
Close
|
|
</button>
|
|
<button
|
|
onClick={handlePrint}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
>
|
|
Print Invoice
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Print Styles */}
|
|
<style>
|
|
{`
|
|
@media print {
|
|
body * {
|
|
visibility: hidden;
|
|
}
|
|
#printable-invoice, #printable-invoice * {
|
|
visibility: visible;
|
|
}
|
|
#printable-invoice {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
padding: 20mm;
|
|
}
|
|
.no-print {
|
|
display: none !important;
|
|
}
|
|
}
|
|
`}
|
|
</style>
|
|
</div>
|
|
);
|
|
} |