// Unified Invoice PDF Component // Consolidates PrintInvoice, PdfInvoice, and InvoicePdfGenerator into one reusable component // Supports: Print, Download PDF, Share to WhatsApp - all with proper Marathi font support import React, { useState, useCallback } from 'react'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; import { Printer, Download, Share2, X, FileText } from 'lucide-react'; import { Transaction } from '../types'; import { useToast } from '../context/ToastContext'; import InvoiceBody from './InvoiceBody'; // Import shared utilities import { formatINR, formatDate, getInvoiceMeta, calculateInvoiceTotals, getDisplayItems, formatPotiWeights, calculatePaidAmount, generateSafeFileName, getPaymentBreakdown, getPaymentModeLabel, InvoiceTotals, GroupedInvoiceItem } from '../utils/invoiceUtils'; import { loadDevanagariFonts, applyDevanagariFont, renderMarathiText } from '../utils/pdfFonts'; // ============================================ // TYPES // ============================================ export type InvoiceAction = 'print' | 'download' | 'share'; export interface InvoicePDFProps { transaction: Transaction; className?: string; buttonLabel?: string; showIcon?: boolean; defaultAction?: InvoiceAction; onActionComplete?: (action: InvoiceAction, success: boolean) => void; } interface InvoicePDFState { showPreview: boolean; isGenerating: boolean; isSharing: boolean; } // ============================================ // COMPONENT // ============================================ export const InvoicePDF: React.FC = ({ transaction, className, buttonLabel, showIcon = true, defaultAction = 'print', onActionComplete }) => { const [state, setState] = useState({ showPreview: false, isGenerating: false, isSharing: false }); const toast = useToast(); // Calculate invoice data using shared utilities const meta = getInvoiceMeta(transaction); const totals = calculateInvoiceTotals(transaction); const displayItems = getDisplayItems(transaction); // NO GROUPING const paymentInfo = getPaymentBreakdown(transaction); // ============================================ // PDF GENERATION // ============================================ const generatePDF = useCallback(async (): Promise => { // Load fonts first await loadDevanagariFonts(); const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a5' }); // === HEADER === doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text('INVOICE', 14, 20); // Bill No and Date doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text(`Bill No: ${transaction.bill_number}`, 140, 15, { align: 'right' }); doc.setFontSize(12); doc.setFont('helvetica', 'normal'); doc.text(`Date: ${formatDate(transaction.bill_date)} (${meta.billTypeLabel})`, 140, 22, { align: 'right' }); // Header line doc.setLineWidth(2); doc.line(14, 28, 140, 28); // === BILLED TO — BIGGER PARTY NAME === doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.setTextColor(85, 85, 85); doc.text('BILLED TO', 14, 38); doc.setLineWidth(0.5); doc.line(14, 39, 45, 39); doc.setTextColor(0, 0, 0); doc.setFontSize(22); // BIGGER doc.setFont('helvetica', 'bold'); doc.text(transaction.party_name || '-', 14, 48); const partyPhone = (transaction as any).party_phone; if (partyPhone) { doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.setTextColor(85, 85, 85); doc.text(`Phone: ${partyPhone}`, 14, 55); doc.setTextColor(0, 0, 0); } // === ITEMS TABLE — NO GROUPING === const tableData = displayItems.map((item, idx) => { const potiWeights = formatPotiWeights(item); const amount = item.net_weight * item.rate_per_kg; return [ idx + 1, `${item.mirchi_name}\nWeights: ${potiWeights}`, item.poti_count, item.net_weight.toFixed(2), formatINR(item.rate_per_kg), formatINR(amount) ]; }); autoTable(doc, { startY: 60, head: [['#', 'मिरची', 'पोती', 'वजन', 'दर', 'एकूण रक्कम']], body: tableData, theme: 'grid', styles: { fontSize: 10, cellPadding: 4, lineWidth: 0.5, lineColor: [0, 0, 0], font: 'NotoSansDevanagari' }, headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold', fontSize: 12 }, bodyStyles: { textColor: [0, 0, 0] }, columnStyles: { 0: { halign: 'center', cellWidth: 10 }, 1: { halign: 'left', cellWidth: 45 }, 2: { halign: 'center', cellWidth: 15 }, 3: { halign: 'center', cellWidth: 20 }, 4: { halign: 'right', cellWidth: 20 }, 5: { halign: 'right', cellWidth: 22 } } }); const finalY = (doc as any).lastAutoTable.finalY + 10; // === SUMMARY TABLE === const summaryData: string[][] = [ ['पॅकिंग', formatINR(totals.packing)], ['एकूण रक्कम', formatINR(totals.subtotal + totals.packing)] ]; if (totals.cess > 0) summaryData.push(['सेस', formatINR(totals.cess)]); if (totals.adat > 0) summaryData.push(['अडत', formatINR(totals.adat)]); if (totals.hamali > 0) summaryData.push(['हमाली', formatINR(totals.hamali)]); if (totals.gaadiBharni > 0) summaryData.push(['गाडी भाडे', formatINR(totals.gaadiBharni)]); if (totals.otherExpenses > 0) summaryData.push(['इतर खर्च', formatINR(totals.otherExpenses)]); summaryData.push(['पूर्ण रक्कम', formatINR(totals.grandTotal)]); // Append Payments to Summary Data const { payments: payBreakdown, totalPaid, totalDue, isPartial } = paymentInfo; if (payBreakdown.length > 0) { summaryData.push([isPartial ? 'जमा (Partial)' : 'जमा', formatINR(totalPaid)]); payBreakdown.forEach(p => { summaryData.push([` ${p.modeLabel} ${p.reference ? `(${p.reference})` : ''}`, formatINR(p.amount)]); }); } else { summaryData.push(['जमा', formatINR(0)]); } if (totalDue > 0) { summaryData.push(['येणे बाकी', formatINR(totalDue)]); } autoTable(doc, { startY: finalY, body: summaryData, theme: 'grid', styles: { fontSize: 10, cellPadding: 3, lineWidth: 0.5, lineColor: [200, 200, 200], font: 'NotoSansDevanagari' }, bodyStyles: { textColor: [0, 0, 0] }, columnStyles: { 0: { halign: 'left', cellWidth: 35 }, 1: { halign: 'right', cellWidth: 30 } }, margin: { left: 85 } }); return doc; }, [transaction, meta, totals, displayItems, paymentInfo]); // ============================================ // ACTIONS // ============================================ const handlePrint = useCallback(async () => { setState(prev => ({ ...prev, isGenerating: true })); try { const doc = await generatePDF(); doc.autoPrint(); window.open(doc.output('bloburl'), '_blank'); toast.success('Invoice sent to printer'); onActionComplete?.('print', true); } catch (error) { console.error('Print failed:', error); toast.error('Print failed', 'Please try again'); onActionComplete?.('print', false); } finally { setState(prev => ({ ...prev, isGenerating: false })); } }, [generatePDF, toast, onActionComplete]); const handleDownload = useCallback(async () => { setState(prev => ({ ...prev, isGenerating: true })); try { const doc = await generatePDF(); const fileName = generateSafeFileName( transaction.party_name || 'Party', transaction.bill_number, 'Invoice' ); doc.save(fileName); toast.success('Invoice downloaded'); onActionComplete?.('download', true); } catch (error) { console.error('Download failed:', error); toast.error('Download failed', 'Please try again'); onActionComplete?.('download', false); } finally { setState(prev => ({ ...prev, isGenerating: false })); } }, [generatePDF, transaction, toast, onActionComplete]); const handleShare = useCallback(async () => { setState(prev => ({ ...prev, isSharing: true })); try { const doc = await generatePDF(); const pdfBlob = doc.output('blob'); const fileName = generateSafeFileName( transaction.party_name || 'Party', transaction.bill_number, 'Invoice' ); // Try Web Share API first if (navigator.share) { try { const file = new File([pdfBlob], fileName, { type: 'application/pdf' }); if (navigator.canShare && navigator.canShare({ files: [file] })) { await navigator.share({ files: [file], title: `Invoice ${transaction.bill_number}`, text: `Invoice for ${transaction.party_name || 'Customer'} - Bill #${transaction.bill_number}` }); toast.success('Invoice shared'); onActionComplete?.('share', true); return; } } catch (err: any) { if (err.name === 'AbortError') { // User cancelled return; } } } // Fallback: download + WhatsApp link const url = URL.createObjectURL(pdfBlob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); const text = encodeURIComponent( `Invoice #${transaction.bill_number}\nType: ${meta.isMixed ? 'Mixed' : (meta.isPatti ? 'Patti' : 'Regular')}\nParty: ${transaction.party_name || ''}\nAmount: ₹${Math.round(totals.grandTotal).toLocaleString()}\nDate: ${formatDate(transaction.bill_date)}\n\nPlease find the invoice attached.` ); window.open(`https://wa.me/?text=${text}`, '_blank'); toast.success('Invoice downloaded', 'Share via WhatsApp opened'); onActionComplete?.('share', true); } catch (error) { console.error('Share failed:', error); toast.error('Share failed', 'Please try again'); onActionComplete?.('share', false); } finally { setState(prev => ({ ...prev, isSharing: false })); } }, [generatePDF, transaction, meta, totals, toast, onActionComplete]); // ============================================ // RENDER // ============================================ return ( <> {state.showPreview && (
{/* Header */}

Invoice Preview - {transaction.bill_number}

{/* Preview Content — Uses SAME InvoiceBody */}
)} ); }; export default InvoicePDF;