pratikfrontend / utils /InvoicePdfGenerator.ts
Antaram's picture
Upload 50 files
c2d9a04 verified
Raw
History Blame Contribute Delete
7.71 kB
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