import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; import { Transaction, PattiExpenses } from '../types'; export const formatINR = (v: number | string | null | undefined) => { const numeric = typeof v === 'number' ? v : Number(v); return `₹${(Number.isFinite(numeric) ? numeric : 0).toFixed(2)}`; }; export const formatDate = (dateStr: string) => { const date = new Date(dateStr); return date.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' }); }; const getPaymentModeLabel = (mode: string): string => { switch (mode.toLowerCase()) { case 'cash': return 'रोख (Cash)'; case 'online': return 'ऑनलाइन (Online)'; case 'cheque': return 'चेक (Cheque)'; default: return mode; } }; export const generateInvoicePDFDoc = (transaction: Transaction): jsPDF => { const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a5' }); // Calculate totals - handle both regular and Patti expense formats const isPatti = (transaction as any).bill_type === 'patti_jawaak' || (transaction as any).bill_type === 'patti_awaak'; const isMixed = !!(transaction as any).is_mixed; const billTypeLabel = isMixed ? 'M' : (isPatti ? 'P' : 'R'); const pattiExp = (transaction as any).expenses as PattiExpenses | undefined; const subtotal = transaction.subtotal || 0; const packing = isPatti && pattiExp ? (pattiExp.packing || 0) : (transaction.expenses?.poti_amount || 0); const cess = isPatti && pattiExp ? (pattiExp.godown || 0) : (transaction.expenses?.cess_amount || 0); const adat = isPatti && pattiExp ? (pattiExp.commission || 0) : (transaction.expenses?.adat_amount || 0); const hamali = isPatti && pattiExp ? ((pattiExp.hamali || 0)) : ((transaction.expenses?.hamali_amount || 0) + (transaction.expenses?.packaging_hamali_amount || 0)); const gaadiBharni = isPatti && pattiExp ? (pattiExp.gaadi_bhade || 0) : (transaction.expenses?.gaadi_bharni || 0); const otherExpenses = (isPatti && pattiExp ? pattiExp.other_expenses : transaction.expenses?.other_expenses) || 0; const grandTotal = transaction.total_amount || (subtotal + packing + cess + adat + hamali + gaadiBharni + otherExpenses); // NO GROUPING - each item shown separately (client requirement) const displayItems = transaction.items; // === INVOICE HEADER === doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text('INVOICE', 14, 20); // Bill No and Date on right 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)} (${billTypeLabel})`, 140, 22, { align: 'right' }); // Header line doc.setLineWidth(2); doc.line(14, 28, 140, 28); // === PARTY SECTION — CENTERED === const partyPhone = (transaction as any).party_phone; const actualPayments = (transaction.payments || []).filter((p: any) => p.mode !== 'due' && p.mode !== 'Due'); const actualPaidAmount = actualPayments.reduce((sum: number, p: any) => sum + p.amount, 0); const calculatedDue = grandTotal - actualPaidAmount; const isPartial = actualPaidAmount > 0 && calculatedDue > 0; doc.setTextColor(0, 0, 0); doc.setFontSize(22); doc.setFont('helvetica', 'bold'); // Center logic const partyNameText = transaction.party_name || '-'; const payMethodText = (!isPartial && calculatedDue <= 0 && transaction.payment_method) ? ` (${transaction.payment_method})` : ''; const fullText = partyNameText + payMethodText; doc.text(fullText, 74, 38, { align: 'center' }); if (partyPhone) { doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.setTextColor(85, 85, 85); doc.text(`Mob: ${partyPhone}`, 74, 43, { align: 'center' }); doc.setTextColor(0, 0, 0); } let nextY = 48; // === ITEMS TABLE — NO GROUPING === const tableData = displayItems.map((item: any, idx: number) => { const potiWeights = Array.isArray(item.poti_weights) ? item.poti_weights.join(', ') : (typeof item.poti_weights === 'string' ? item.poti_weights : '-'); const amount = (item.net_weight || 0) * (item.rate_per_kg || 0); const lotLabel = item.lot_number ? ` (${item.lot_number})` : ''; return [ idx + 1, `${item.mirchi_name || '-'}${lotLabel}\nWeights: ${potiWeights}`, item.poti_count || 0, (item.net_weight || 0).toFixed(2), formatINR(item.rate_per_kg), formatINR(amount) ]; }); autoTable(doc, { startY: nextY, head: [['#', 'मिरची', 'पोती', 'वजन', 'दर', 'एकूण']], body: tableData, theme: 'grid', styles: { fontSize: 10, cellPadding: 4, lineWidth: 0.5, lineColor: [0, 0, 0] }, 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 (Right side) === const summaryData: string[][] = []; summaryData.push(['पॅकिंग', formatINR(packing)]); summaryData.push(['एकूण', formatINR(subtotal + packing)]); if (cess > 0) summaryData.push(['सेस', formatINR(cess)]); if (adat > 0) summaryData.push(['अडत', formatINR(adat)]); if (hamali > 0) summaryData.push(['हमाली', formatINR(hamali)]); if (gaadiBharni > 0) summaryData.push(['गाडी भाडे', formatINR(gaadiBharni)]); if (otherExpenses > 0) summaryData.push(['इतर खर्च', formatINR(otherExpenses)]); summaryData.push(['पूर्ण रक्कम', formatINR(grandTotal)]); // === PAYMENT SUMMARY — WITH PAYMENT MODE === const actualPay = (transaction.payments || []).filter((p: any) => p.mode !== 'due' && p.mode !== 'Due'); const actualP = actualPay.reduce((sum: number, p: any) => sum + p.amount, 0); const calcDue = grandTotal - actualP; const isP = actualP > 0 && calcDue > 0; if (actualPay.length > 0) { summaryData.push([isP ? 'जमा (Partial)' : 'जमा', formatINR(actualP)]); actualPay.forEach((p: any) => { summaryData.push([`${getPaymentModeLabel(p.mode)} ${p.reference ? `(${p.reference})` : ''}`, formatINR(p.amount)]); }); } else { summaryData.push(['जमा', formatINR(0)]); } if (calcDue > 0) { summaryData.push(['येणे बाकी', formatINR(calcDue)]); } 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; }; // shareInvoiceToWhatsApp is now handled exclusively in invoiceUtils.ts