Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { Transaction } from '../types'; | |
| import { Download, X, Eye } from 'lucide-react'; | |
| import jsPDF from 'jspdf'; | |
| import 'jspdf-autotable'; | |
| interface PdfInvoiceProps { | |
| transaction: Transaction; | |
| className?: string; | |
| children?: React.ReactNode; | |
| } | |
| const PdfInvoice: React.FC<PdfInvoiceProps> = ({ transaction, className, children }) => { | |
| const [showPreview, setShowPreview] = useState(false); | |
| const [pdfUrl, setPdfUrl] = useState<string>(''); | |
| const formatINR = (v: number) => `₹${v.toFixed(2)}`; | |
| const formatDate = (dateStr: string) => { | |
| const date = new Date(dateStr); | |
| return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' }); | |
| }; | |
| const generatePDF = (download: boolean = false) => { | |
| const doc = new jsPDF(); | |
| const subtotal = transaction.subtotal || 0; | |
| const packing = transaction.expenses?.poti_amount || 0; | |
| const cess = transaction.expenses?.cess_amount || 0; | |
| const adat = transaction.expenses?.adat_amount || 0; | |
| const hamali = (transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0); | |
| const grandTotal = transaction.total_amount || 0; | |
| const paidAmount = transaction.paid_amount || 0; | |
| const balanceAmount = transaction.balance_amount || 0; | |
| // Header - Invoice Date on left, Bill No on right | |
| doc.setFontSize(13); | |
| doc.setFont('helvetica', 'bold'); | |
| doc.text(`Invoice Date: ${formatDate(transaction.bill_date)}`, 14, 15); | |
| doc.text(`Bill No: ${transaction.bill_number}`, 196, 15, { align: 'right' }); | |
| // Party Details Section - LEFT ALIGNED | |
| doc.setFontSize(11); | |
| doc.setFont('helvetica', 'bold'); | |
| doc.text('Party Details', 14, 25); | |
| doc.setLineWidth(0.5); | |
| doc.line(14, 26, 55, 26); | |
| // Party info - explicitly left aligned at x=14 | |
| doc.setFont('helvetica', 'normal'); | |
| doc.setFontSize(9); | |
| const partyName = transaction.party_name || '-'; | |
| const partyCity = transaction.party_city || '-'; | |
| const partyPhone = transaction.party_phone || '-'; | |
| doc.text(`Name: ${partyName}`, 14, 31, { align: 'left' }); | |
| doc.text(`Place: ${partyCity}`, 14, 36, { align: 'left' }); | |
| doc.text(`Mobile: ${partyPhone}`, 14, 41, { align: 'left' }); | |
| // Items Table | |
| const tableData = transaction.items.map((item, idx) => { | |
| const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-'); | |
| const amount = item.net_weight * item.rate_per_kg; | |
| return [idx + 1, `${item.mirchi_name}\n${potiWeights}`, item.poti_count, item.net_weight, item.rate_per_kg, formatINR(amount)]; | |
| }); | |
| (doc as any).autoTable({ | |
| startY: 47, | |
| head: [['#', 'Product Type', 'Bags', 'Net (kg)', 'Rate (₹)', 'Amount (₹)']], | |
| body: tableData, | |
| theme: 'grid', | |
| styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] }, | |
| headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold' }, | |
| bodyStyles: { textColor: [0, 0, 0] }, | |
| columnStyles: { | |
| 0: { halign: 'center', cellWidth: 10 }, | |
| 1: { halign: 'left', cellWidth: 60 }, | |
| 2: { halign: 'center', cellWidth: 20 }, | |
| 3: { halign: 'center', cellWidth: 25 }, | |
| 4: { halign: 'center', cellWidth: 25 }, | |
| 5: { halign: 'right', cellWidth: 35 } | |
| } | |
| }); | |
| // Summary Table - Right side | |
| const finalY = (doc as any).lastAutoTable.finalY + 5; | |
| const summaryData = [ | |
| ['Sub Total', formatINR(subtotal)], | |
| ['Packing', formatINR(packing)], | |
| ['CESS', formatINR(cess)], | |
| ['Adat', formatINR(adat)], | |
| ['Hamali', formatINR(hamali)], | |
| ['Total Amount', formatINR(grandTotal)] | |
| ]; | |
| (doc as any).autoTable({ | |
| startY: finalY, | |
| body: summaryData, | |
| theme: 'grid', | |
| styles: { fontSize: 8, cellPadding: 2, lineWidth: 0.5, lineColor: [0, 0, 0] }, | |
| bodyStyles: { textColor: [0, 0, 0] }, | |
| columnStyles: { | |
| 0: { halign: 'left', cellWidth: 50 }, | |
| 1: { halign: 'right', cellWidth: 35 } | |
| }, | |
| margin: { left: 111 } | |
| }); | |
| // Payment Status Section - LEFT ALIGNED | |
| const paymentY = (doc as any).lastAutoTable.finalY + 10; | |
| doc.setFontSize(11); | |
| doc.setFont('helvetica', 'bold'); | |
| doc.text('Payment Status', 14, paymentY, { align: 'left' }); | |
| doc.setLineWidth(0.5); | |
| doc.line(14, paymentY + 1, 60, paymentY + 1); | |
| doc.setFont('helvetica', 'normal'); | |
| doc.setFontSize(9); | |
| let currentY = paymentY + 6; | |
| if (transaction.payments && transaction.payments.length > 0) { | |
| transaction.payments.forEach((payment) => { | |
| const mode = payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode; | |
| doc.text(`${mode}: ${formatINR(payment.amount)}`, 14, currentY, { align: 'left' }); | |
| currentY += 5; | |
| }); | |
| doc.setFont('helvetica', 'bold'); | |
| doc.text(`Total Paid: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' }); | |
| currentY += 5; | |
| } else { | |
| doc.text(`Paid Amount: ${formatINR(paidAmount)}`, 14, currentY, { align: 'left' }); | |
| currentY += 5; | |
| } | |
| if (balanceAmount > 0) { | |
| doc.setFont('helvetica', 'bold'); | |
| doc.setTextColor(211, 47, 47); | |
| doc.text(`Due Amount: ${formatINR(balanceAmount)}`, 14, currentY, { align: 'left' }); | |
| doc.setTextColor(0, 0, 0); | |
| } | |
| // Signature - Right side | |
| doc.setFont('helvetica', 'normal'); | |
| doc.setFontSize(9); | |
| doc.text('(Authorised Signatory)', 196, paymentY + 20, { align: 'right' }); | |
| if (download) { | |
| doc.save(`Invoice_${transaction.bill_number}.pdf`); | |
| } else { | |
| const pdfBlob = doc.output('blob'); | |
| const url = URL.createObjectURL(pdfBlob); | |
| setPdfUrl(url); | |
| setShowPreview(true); | |
| } | |
| }; | |
| const handleDownload = () => { | |
| generatePDF(true); | |
| setShowPreview(false); | |
| if (pdfUrl) { | |
| URL.revokeObjectURL(pdfUrl); | |
| } | |
| }; | |
| const handleClose = () => { | |
| setShowPreview(false); | |
| if (pdfUrl) { | |
| URL.revokeObjectURL(pdfUrl); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <button | |
| onClick={() => generatePDF(false)} | |
| className={className || "p-2 bg-white text-red-600 border border-red-600 rounded-md hover:bg-red-50 transition-colors flex items-center justify-center shadow-sm"} | |
| title="Preview & Download PDF" | |
| > | |
| {children || <Eye size={16} />} | |
| </button> | |
| {showPreview && ( | |
| <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"> | |
| <div className="bg-white rounded-lg shadow-2xl max-w-6xl w-full max-h-[90vh] flex flex-col"> | |
| <div className="border-b border-gray-200 px-6 py-4 flex justify-between items-center"> | |
| <h2 className="text-xl font-bold text-gray-800">PDF Preview</h2> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={handleDownload} | |
| className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2" | |
| > | |
| <Download size={18} /> | |
| Download PDF | |
| </button> | |
| <button | |
| onClick={handleClose} | |
| className="p-2 hover:bg-gray-100 rounded-lg transition-colors" | |
| > | |
| <X size={24} className="text-gray-600" /> | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-auto p-4 bg-gray-100"> | |
| <iframe | |
| src={pdfUrl} | |
| className="w-full h-full min-h-[600px] bg-white rounded shadow-lg" | |
| title="PDF Preview" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| ); | |
| }; | |
| export default PdfInvoice; |