Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { Transaction } from '../types'; | |
| import { Printer, X } from 'lucide-react'; | |
| interface PrintInvoiceProps { | |
| transaction: Transaction; | |
| className?: string; | |
| } | |
| const PrintInvoice: React.FC<PrintInvoiceProps> = ({ transaction, className }) => { | |
| const [showPreview, setShowPreview] = useState(false); | |
| 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' }); | |
| }; | |
| // Calculate totals | |
| 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 gaadiBharni = transaction.expenses?.gaadi_bharni || 0; | |
| const otherExpenses = transaction.expenses?.other_expenses || 0; | |
| const grandTotal = transaction.total_amount || (subtotal + packing + cess + adat + hamali + gaadiBharni + otherExpenses); | |
| const paidAmount = transaction.paid_amount || 0; | |
| const balanceAmount = transaction.balance_amount || 0; | |
| const handlePrint = () => { | |
| const printContent = document.getElementById('invoice-content'); | |
| if (!printContent) return; | |
| const iframe = document.createElement('iframe'); | |
| iframe.style.position = 'absolute'; | |
| iframe.style.width = '0'; | |
| iframe.style.height = '0'; | |
| iframe.style.border = 'none'; | |
| document.body.appendChild(iframe); | |
| const doc = iframe.contentWindow?.document; | |
| if (doc) { | |
| doc.open(); | |
| doc.write(` | |
| <html> | |
| <head> | |
| <title>Invoice-${transaction.bill_number}</title> | |
| <style> | |
| /* Reset margins to absolute zero */ | |
| @page { size: A4; margin: 0; } | |
| body { margin: 0; padding: 0; background-color: white; font-family: Helvetica, Arial, sans-serif; } | |
| /* Overwrite the container style for printing */ | |
| #invoice-container-inner { | |
| width: 210mm !important; | |
| /* Reducing slightly from 297mm to 295mm prevents the 'spillover' blank page */ | |
| min-height: 295mm !important; | |
| padding: 15mm !important; | |
| margin: 0 auto !important; | |
| box-sizing: border-box !important; | |
| overflow: hidden !important; /* Cut off any rogue pixels */ | |
| } | |
| /* Helper to hide non-print elements if any sneak in */ | |
| .print-hidden { display: none !important; } | |
| </style> | |
| </head> | |
| <body> | |
| ${printContent.innerHTML} | |
| </body> | |
| </html> | |
| `); | |
| doc.close(); | |
| iframe.contentWindow?.focus(); | |
| setTimeout(() => { | |
| iframe.contentWindow?.print(); | |
| setTimeout(() => { | |
| document.body.removeChild(iframe); | |
| }, 1000); | |
| }, 500); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <button | |
| onClick={() => setShowPreview(true)} | |
| className={className || "p-2 bg-white text-teal-600 border border-teal-600 rounded-md hover:bg-teal-50 transition-colors flex items-center justify-center shadow-sm"} | |
| title="Print Invoice" | |
| > | |
| <Printer size={16} /> | |
| </button> | |
| {showPreview && ( | |
| <div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-2 sm:p-4"> | |
| <div className="bg-white rounded-lg shadow-2xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden flex flex-col"> | |
| {/* Header */} | |
| <div className="sticky top-0 bg-white border-b border-gray-200 px-3 py-3 sm:px-6 sm:py-4 flex justify-between items-center z-10 shrink-0"> | |
| <h2 className="text-lg sm:text-xl font-bold text-gray-800 truncate mr-2">Invoice Preview</h2> | |
| <div className="flex gap-2 items-center"> | |
| <button | |
| onClick={handlePrint} | |
| className="px-3 py-1.5 sm:px-4 sm:py-2 text-sm sm:text-base bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-1 sm:gap-2" | |
| > | |
| <Printer size={16} className="sm:w-[18px] sm:h-[18px]" /> | |
| <span>Print</span> | |
| </button> | |
| <button | |
| onClick={() => setShowPreview(false)} | |
| className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg transition-colors" | |
| > | |
| <X size={20} className="text-gray-600 sm:w-6 sm:h-6" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Scrollable Area */} | |
| <div className="overflow-auto flex-1 p-2 sm:p-8 bg-gray-100"> | |
| <div className="mb-4 text-xs text-gray-500 text-center sm:hidden"> | |
| Scroll horizontally to view full invoice | |
| </div> | |
| {/* Wrapper for ID targeting */} | |
| <div id="invoice-content"> | |
| <div | |
| id="invoice-container-inner" | |
| className="bg-white mx-auto shadow-sm" | |
| style={{ | |
| width: '210mm', | |
| minHeight: '297mm', /* This stays 297mm for screen preview, but Print overrides it to 295mm */ | |
| padding: '15mm', | |
| margin: 'auto', | |
| fontFamily: 'Helvetica, Arial, sans-serif', | |
| fontSize: '12px', | |
| color: '#000', | |
| backgroundColor: '#fff', | |
| boxSizing: 'border-box', | |
| position: 'relative' | |
| }} | |
| > | |
| {/* Invoice Header */} | |
| <div style={{ borderBottom: '2px solid #333', paddingBottom: '10px', marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}> | |
| <div> | |
| <h1 style={{ margin: 0, fontSize: '24px', fontWeight: 'bold', letterSpacing: '1px' }}>INVOICE</h1> | |
| </div> | |
| <div style={{ textAlign: 'right' }}> | |
| {/* <div style={{ fontSize: '14px', fontWeight: 'bold' }}>Bill No: {transaction.bill_number}</div> */} | |
| <div style={{ fontSize: '12px', marginTop: '4px' }}>Date: {formatDate(transaction.bill_date)}</div> | |
| </div> | |
| </div> | |
| {/* Info Section */} | |
| <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}> | |
| <div style={{ width: '55%', textAlign: 'left' }}> | |
| <div style={{ | |
| textTransform: 'uppercase', | |
| fontSize: '10px', | |
| color: '#555', | |
| fontWeight: 'bold', | |
| marginBottom: '4px', | |
| borderBottom: '1px solid #eee', | |
| paddingBottom: '2px', | |
| width: '100%' | |
| }}>Billed To</div> | |
| <div style={{ fontSize: '14px', fontWeight: 'bold', marginTop: '5px' }}>{transaction.party_name}</div> | |
| <div style={{ marginTop: '2px' }}>Phone: {transaction.party_phone || '-'}</div> | |
| </div> | |
| </div> | |
| {/* Table */} | |
| <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '20px' }}> | |
| <thead> | |
| <tr style={{ backgroundColor: '#f3f4f6' }}> | |
| <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '5%' }}>#</th> | |
| <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'left', width: '40%' }}>Item Description</th> | |
| <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '10%' }}>Bags</th> | |
| <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'center', width: '15%' }}>Net Weight</th> | |
| <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Rate</th> | |
| <th style={{ border: '1px solid #ccc', padding: '8px 6px', textAlign: 'right', width: '15%' }}>Amount</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {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 ( | |
| <tr key={idx}> | |
| <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{idx + 1}</td> | |
| <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'left' }}> | |
| <div style={{ fontWeight: 'bold' }}>{item.mirchi_name}</div> | |
| <div style={{ fontSize: '9px', color: '#666', marginTop: '2px', lineHeight: '1.2' }}> | |
| Weights: {potiWeights} | |
| </div> | |
| </td> | |
| <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.poti_count}</td> | |
| <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'center' }}>{item.net_weight} kg</td> | |
| <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(item.rate_per_kg)}</td> | |
| <td style={{ border: '1px solid #ccc', padding: '6px', textAlign: 'right' }}>{formatINR(amount)}</td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| {/* Totals Section */} | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> | |
| <div style={{ width: '50%', paddingRight: '20px', textAlign: 'left' }}> | |
| <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '4px' }}> | |
| <div style={{ fontWeight: 'bold', borderBottom: '1px solid #eee', marginBottom: '8px', paddingBottom: '4px' }}>Payment Status</div> | |
| {transaction.payments && transaction.payments.length > 0 ? ( | |
| <> | |
| {transaction.payments.map((payment, idx) => ( | |
| <div key={idx} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '11px', marginBottom: '2px' }}> | |
| <span>{payment.mode === 'cash' ? 'Cash' : payment.mode === 'online' ? 'Online' : payment.mode}:</span> | |
| <span>{formatINR(payment.amount)}</span> | |
| </div> | |
| ))} | |
| <div style={{ borderTop: '1px dashed #ccc', marginTop: '6px', paddingTop: '6px', display: 'flex', justifyContent: 'space-between', fontWeight: 'bold' }}> | |
| <span>Total Paid:</span> | |
| <span>{formatINR(paidAmount)}</span> | |
| </div> | |
| </> | |
| ) : ( | |
| <div style={{ display: 'flex', justifyContent: 'space-between' }}> | |
| <span>Total Paid:</span> | |
| <span>{formatINR(paidAmount)}</span> | |
| </div> | |
| )} | |
| {balanceAmount > 0 && ( | |
| <div style={{ marginTop: '8px', color: '#c62828', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}> | |
| <span>Due Amount:</span> | |
| <span>{formatINR(balanceAmount)}</span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div style={{ width: '45%' }}> | |
| <table style={{ width: '100%', borderCollapse: 'collapse' }}> | |
| <tbody> | |
| {/* Renamed Sub Total to Total */} | |
| {/* <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee' }}>Total</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>{formatINR(subtotal)}</td> | |
| </tr> */} | |
| <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Packing Expenses</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(packing)}</td> | |
| </tr> | |
| {/* New Row: Total + Packing Calculation */} | |
| <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>Sub Total</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee', fontWeight: 'bold' }}>{formatINR(subtotal + packing)}</td> | |
| </tr> | |
| <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>CESS</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(cess)}</td> | |
| </tr> | |
| <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Adat</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(adat)}</td> | |
| </tr> | |
| <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Hamali</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(hamali)}</td> | |
| </tr> | |
| <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Gaadi Bharni</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(gaadiBharni)}</td> | |
| </tr> | |
| <tr> | |
| <td style={{ padding: '4px', textAlign: 'left', borderBottom: '1px solid #eee', color: '#555' }}>Other Expenses</td> | |
| <td style={{ padding: '4px', textAlign: 'right', borderBottom: '1px solid #eee' }}>{formatINR(otherExpenses)}</td> | |
| </tr> | |
| <tr style={{ backgroundColor: '#f3f4f6' }}> | |
| <th style={{ padding: '8px 4px', textAlign: 'left', borderTop: '2px solid #000', fontSize: '14px' }}>Grand Total</th> | |
| <th style={{ padding: '8px 4px', textAlign: 'right', borderTop: '2px solid #000', fontSize: '14px' }}>{formatINR(grandTotal)}</th> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {/* Signatures */} | |
| <div style={{ marginTop: '50px', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}> | |
| <div style={{ textAlign: 'left', fontSize: '10px', color: '#666' }}> | |
| <p>Thank you for your business.</p> | |
| </div> | |
| <div style={{ textAlign: 'center' }}> | |
| <div style={{ marginBottom: '40px', fontSize: '10px' }}>For Authorised Signatory</div> | |
| <div style={{ borderTop: '1px solid #000', width: '150px', margin: 'auto' }}></div> | |
| <div style={{ fontSize: '10px', marginTop: '4px' }}>(Signature)</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| ); | |
| }; | |
| export default PrintInvoice; |