Spaces:
Sleeping
Sleeping
| 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 | |