Spaces:
Sleeping
Sleeping
| import { useState, useEffect } from 'react'; | |
| import { Download, Eye, Loader2 } from 'lucide-react'; | |
| import api from '../api/api'; | |
| import { downloadInvoiceAsPDF, downloadInvoiceAsJSON } from '../utils/invoiceGenerator'; | |
| const InvoiceView = ({ invoiceId, photographer, customer }) => { | |
| const [invoice, setInvoice] = useState(null); | |
| const [loading, setLoading] = useState(true); | |
| useEffect(() => { | |
| fetchInvoice(); | |
| }, [invoiceId]); | |
| const fetchInvoice = async () => { | |
| try { | |
| const res = await api.get(`/invoices/${invoiceId}`); | |
| setInvoice(res.data); | |
| } catch (err) { | |
| console.error('Failed to load invoice:', err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center justify-center py-8"> | |
| <Loader2 className="animate-spin text-blue-600" size={28} /> | |
| </div> | |
| ); | |
| } | |
| if (!invoice) { | |
| return <div className="text-center text-gray-600">Invoice not found</div>; | |
| } | |
| const statusColor = { | |
| pending: 'amber', | |
| paid: 'green', | |
| overdue: 'red', | |
| }; | |
| const color = statusColor[invoice.paymentStatus]; | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Header */} | |
| <div className="bg-white border border-gray-200 rounded-lg p-6"> | |
| <div className="flex items-center justify-between mb-4"> | |
| <div> | |
| <h2 className="text-2xl font-bold text-gray-900">Invoice</h2> | |
| <p className="text-lg text-gray-600 font-semibold">{invoice.invoiceNumber}</p> | |
| </div> | |
| <div className={`px-4 py-2 rounded-lg bg-${color}-100 text-${color}-700 font-bold capitalize`}> | |
| {invoice.paymentStatus} | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> | |
| <div> | |
| <p className="text-gray-500">Issue Date</p> | |
| <p className="font-semibold">{new Date(invoice.issueDate).toLocaleDateString()}</p> | |
| </div> | |
| <div> | |
| <p className="text-gray-500">Due Date</p> | |
| <p className="font-semibold">{new Date(invoice.dueDate).toLocaleDateString()}</p> | |
| </div> | |
| <div> | |
| <p className="text-gray-500">From</p> | |
| <p className="font-semibold">{photographer?.firstName} {photographer?.lastName}</p> | |
| </div> | |
| <div> | |
| <p className="text-gray-500">To</p> | |
| <p className="font-semibold">{customer?.firstName} {customer?.lastName}</p> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Items Table */} | |
| <div className="bg-white border border-gray-200 rounded-lg overflow-hidden"> | |
| <table className="w-full"> | |
| <thead className="bg-gray-50 border-b"> | |
| <tr> | |
| <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700">Description</th> | |
| <th className="px-6 py-3 text-right text-sm font-semibold text-gray-700">Quantity</th> | |
| <th className="px-6 py-3 text-right text-sm font-semibold text-gray-700">Unit Price</th> | |
| <th className="px-6 py-3 text-right text-sm font-semibold text-gray-700">Total</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {invoice.items?.map((item, idx) => ( | |
| <tr key={idx} className="border-b hover:bg-gray-50"> | |
| <td className="px-6 py-3 text-gray-900">{item.description}</td> | |
| <td className="px-6 py-3 text-right text-gray-900">{item.quantity}</td> | |
| <td className="px-6 py-3 text-right text-gray-900">₹{item.unitPrice.toFixed(2)}</td> | |
| <td className="px-6 py-3 text-right text-gray-900">₹{item.total.toFixed(2)}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| {/* Totals */} | |
| <div className="bg-gray-50 px-6 py-4 space-y-2 text-sm"> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-700">Subtotal</span> | |
| <span className="font-semibold text-gray-900">₹{invoice.subtotal.toFixed(2)}</span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-gray-700">Tax ({invoice.taxRate}%)</span> | |
| <span className="font-semibold text-gray-900">₹{invoice.tax.toFixed(2)}</span> | |
| </div> | |
| {invoice.discount > 0 && ( | |
| <div className="flex justify-between"> | |
| <span className="text-gray-700">Discount</span> | |
| <span className="font-semibold text-gray-900">-₹{invoice.discount.toFixed(2)}</span> | |
| </div> | |
| )} | |
| <div className="border-t pt-2 flex justify-between text-base font-bold"> | |
| <span className="text-gray-900">Total Amount</span> | |
| <span className="text-blue-600">₹{invoice.totalAmount.toFixed(2)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Action Buttons */} | |
| <div className="flex gap-3"> | |
| <button | |
| onClick={() => downloadInvoiceAsPDF(invoice, photographer, customer)} | |
| className="flex-1 flex items-center justify-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 font-medium" | |
| > | |
| <Download size={18} /> Download PDF | |
| </button> | |
| <button | |
| onClick={() => downloadInvoiceAsJSON(invoice)} | |
| className="flex-1 flex items-center justify-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 font-medium" | |
| > | |
| <Eye size={18} /> Export JSON | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default InvoiceView; | |