import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; import { Party, Transaction } from '../types'; // --- CONFIGURATION --- const BG_COLOR: [number, number, number] = [240, 234, 214]; // Beige const BORDER_COLOR: [number, number, number] = [0, 0, 0]; // Black // Helper: Currency Format const FORMAT_CURRENCY = (amount: number) => { return new Intl.NumberFormat('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount); }; // Helper: Date Format const FORMAT_DATE = (dateStr: string) => { if (!dateStr) return ""; const dateObj = PARSE_DATE(dateStr) || new Date(dateStr); return `${dateObj.getDate().toString().padStart(2, '0')}/${(dateObj.getMonth() + 1).toString().padStart(2, '0')}/${dateObj.getFullYear().toString().slice(-2)}`; }; const PARSE_DATE = (value?: string): Date | undefined => { if (!value) return undefined; const trimmed = String(value).trim(); if (!trimmed) return undefined; const iso = trimmed.match(/^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$/); if (iso) { const d = new Date(trimmed); return isNaN(d.getTime()) ? undefined : d; } const slash = trimmed.match(/^\d{1,2}\/\d{1,2}\/(\d{2}|\d{4})$/); if (slash) { const [aStr, bStr, yyStr] = trimmed.split('/'); const a = Number(aStr); const b = Number(bStr); const yy = Number(yyStr); const year = yyStr.length === 2 ? 2000 + yy : yy; if (!a || !b || !year) return undefined; // Prefer en-IN DD/MM/YYYY, but support MM/DD/YYYY when clearly indicated. // - If first part > 12 -> it's DD/MM // - If second part > 12 -> it's MM/DD // - If both <= 12 -> default DD/MM const day = a > 12 ? a : b > 12 ? b : a; const month = a > 12 ? b : b > 12 ? a : b; const d = new Date(year, month - 1, day); return isNaN(d.getTime()) ? undefined : d; } const d = new Date(trimmed); return isNaN(d.getTime()) ? undefined : d; }; const TO_ISO_DATE = (value?: string): string | undefined => { const d = PARSE_DATE(value); return d ? d.toISOString() : undefined; }; const EXTRACT_DATE_FROM_TEXT = (text?: string): string | undefined => { if (!text) return undefined; const iso = text.match(/\b\d{4}-\d{2}-\d{2}\b/); if (iso?.[0]) return TO_ISO_DATE(iso[0]); const mdyOrDmy = text.match(/\b\d{1,2}\/\d{1,2}\/\d{4}\b/); if (!mdyOrDmy?.[0]) return undefined; return TO_ISO_DATE(mdyOrDmy[0]); }; export const generateLedgerPDF = (party: Party, transactions: Transaction[]) => { const doc = new jsPDF(); // --- 1. PAGE CONFIGURATION --- const PAGE_WIDTH = 210; const PAGE_HEIGHT = 297; const MARGIN = 14; const CONTENT_WIDTH = PAGE_WIDTH - (MARGIN * 2); // --- 2. SETUP & BACKGROUND --- doc.setFillColor(...BG_COLOR); doc.rect(0, 0, PAGE_WIDTH, PAGE_HEIGHT, 'F'); // --- 3. DATA PREPARATION --- const ledgerEvents: Array< | { kind: 'bill'; date: string; tx: Transaction } | { kind: 'payment'; date: string; tx: Transaction; payment: any } > = []; for (const tx of transactions) { const billDateIso = TO_ISO_DATE(tx.bill_date) || tx.bill_date; ledgerEvents.push({ kind: 'bill', date: billDateIso, tx }); const payments = tx.payments || []; const validPayments = payments.filter((p: any) => String(p.mode).toLowerCase() !== 'due'); for (const p of validPayments) { const paymentDateRaw = (p as any).payment_date || (p as any).created_at || (p as any).date || EXTRACT_DATE_FROM_TEXT((p as any).reference) || tx.bill_date; const paymentDateIso = TO_ISO_DATE(paymentDateRaw) || TO_ISO_DATE(tx.bill_date) || tx.bill_date; const billTime = (PARSE_DATE(billDateIso) || new Date(billDateIso)).getTime(); const payTime = (PARSE_DATE(paymentDateIso) || new Date(paymentDateIso)).getTime(); const safePaymentDate = payTime < billTime ? billDateIso : paymentDateIso; ledgerEvents.push({ kind: 'payment', date: safePaymentDate, tx, payment: p }); } const recordedPaid = validPayments.reduce((sum: number, p: any) => sum + (Number(p.amount) || 0), 0); const extraPaid = (Number(tx.paid_amount) || 0) - recordedPaid; if (extraPaid > 0) { const paymentDateRaw = (tx as any).updated_at || (tx as any).created_at || tx.bill_date; const paymentDateIso = TO_ISO_DATE(paymentDateRaw) || billDateIso; const billTime = (PARSE_DATE(billDateIso) || new Date(billDateIso)).getTime(); const payTime = (PARSE_DATE(paymentDateIso) || new Date(paymentDateIso)).getTime(); const safePaymentDate = payTime < billTime ? billDateIso : paymentDateIso; ledgerEvents.push({ kind: 'payment', date: safePaymentDate, tx, payment: { mode: 'payment', amount: extraPaid, reference: undefined, __synthetic: true }, }); } } ledgerEvents.sort((a, b) => { const ta = new Date(a.date).getTime(); const tb = new Date(b.date).getTime(); if (ta !== tb) return ta - tb; if (a.kind !== b.kind) return a.kind === 'bill' ? -1 : 1; const na = a.tx.bill_number || ''; const nb = b.tx.bill_number || ''; return na.localeCompare(nb); }); let runningBalance = 0; const tableRows = ledgerEvents.map(ev => { const tx = ev.tx; const typeStr = String(tx.bill_type).toUpperCase(); const numStr = tx.bill_number ? tx.bill_number.toUpperCase() : ""; const isJawaakFromNumber = numStr.startsWith('JAWAAK'); const isAwaakFromNumber = numStr.startsWith('AWAAK'); const isAwaak = isAwaakFromNumber ? true : isJawaakFromNumber ? false : typeStr === 'AWAAK'; if (ev.kind === 'bill') { let itemDetails = ""; if (tx.items && tx.items.length > 0) { tx.items.forEach(item => { let wStr = ""; if (Array.isArray(item.poti_weights)) wStr = item.poti_weights.join(","); else if (item.poti_weights) wStr = String(item.poti_weights); const prefix = itemDetails ? "\n" : ""; itemDetails += `${prefix}${item.mirchi_name}`; if (wStr) itemDetails += ` (${wStr})`; }); } let billParticulars = `No: ${tx.bill_number}`; if (tx.is_return) billParticulars += " (RETURN)"; if (itemDetails) billParticulars += `\n${itemDetails}`; let totalPoti = 0; tx.items?.forEach(i => totalPoti += Number(i.poti_count) || 0); let billDebit = 0; let billCredit = 0; if (isAwaak) { if (tx.is_return) billDebit = tx.total_amount; else billCredit = tx.total_amount; } else { if (tx.is_return) billCredit = tx.total_amount; else billDebit = tx.total_amount; } runningBalance = runningBalance + billDebit - billCredit; return { date: FORMAT_DATE(ev.date), particulars: billParticulars, poti: totalPoti > 0 ? totalPoti.toString() : "-", credit: billCredit > 0 ? FORMAT_CURRENCY(billCredit) : "-", debit: billDebit > 0 ? FORMAT_CURRENCY(billDebit) : "-", balance: `${FORMAT_CURRENCY(Math.abs(runningBalance))} ${runningBalance >= 0 ? 'Dr' : 'Cr'}`, isMainRow: true }; } const p = ev.payment; const amount = Number(p.amount) || 0; let payDebit = 0; let payCredit = 0; if (isAwaak) { if (tx.is_return) payCredit = amount; else payDebit = amount; } else { if (tx.is_return) payDebit = amount; else payCredit = amount; } runningBalance = runningBalance + payDebit - payCredit; const modeStr = String(p.mode || '').charAt(0).toUpperCase() + String(p.mode || '').slice(1); let payDesc = (p as any).__synthetic ? ` [Payment]` : ` [${modeStr}]`; if (p.reference) payDesc += ` ${p.reference}`; return { date: FORMAT_DATE(ev.date), particulars: payDesc, poti: "-", credit: payCredit > 0 ? FORMAT_CURRENCY(payCredit) : "-", debit: payDebit > 0 ? FORMAT_CURRENCY(payDebit) : "-", balance: `${FORMAT_CURRENCY(Math.abs(runningBalance))} ${runningBalance >= 0 ? 'Dr' : 'Cr'}`, isMainRow: false }; }); // --- 4. GENERATE TABLE --- autoTable(doc, { startY: 35, tableWidth: CONTENT_WIDTH, margin: { left: MARGIN, right: MARGIN }, head: [[ 'Date', 'Particulars', 'Poti', 'Credit\n(Jama)', 'Debit\n(Nave)', 'Balance' ]], body: tableRows.map(r => [r.date, r.particulars, r.poti, r.credit, r.debit, r.balance]), theme: 'grid', styles: { fillColor: false, textColor: 0, lineColor: 0, lineWidth: 0.1, font: 'helvetica', fontSize: 9, valign: 'top', cellPadding: 3, overflow: 'linebreak' }, headStyles: { fillColor: false, textColor: 0, fontStyle: 'bold', halign: 'center', valign: 'middle', lineWidth: 0.1, }, columnStyles: { 0: { cellWidth: 20, halign: 'center' }, // Date 1: { cellWidth: 72, halign: 'left' }, // Particulars 2: { cellWidth: 12, halign: 'center' }, // Poti 3: { cellWidth: 26, halign: 'right' }, // Credit 4: { cellWidth: 26, halign: 'right' }, // Debit 5: { cellWidth: 26, halign: 'right', fontStyle: 'bold' } // Balance }, didParseCell: (data) => { const rowIdx = data.row.index; const rowData = tableRows[rowIdx]; if (data.section === 'body' && data.column.index === 1 && rowData?.isMainRow) { data.cell.styles.fontStyle = 'bold'; } }, didDrawPage: (data) => { // Border doc.setDrawColor(...BORDER_COLOR); doc.setLineWidth(0.4); doc.rect(MARGIN, MARGIN, CONTENT_WIDTH, PAGE_HEIGHT - (MARGIN * 2)); // Header if (data.pageNumber === 1) { const startX = MARGIN + 4; const startY = 25; doc.setFontSize(14); doc.setFont("helvetica", "bold"); doc.text("Name of A/c :", startX, startY); const labelWidth = doc.getTextWidth("Name of A/c : "); doc.text(party.name, startX + labelWidth, startY); if (party.city || party.phone) { doc.setFontSize(9); doc.setFont("helvetica", "normal"); const subText = [party.city, party.phone].filter(Boolean).join(" - "); doc.text(subText, startX, startY + 6); } } } }); const cleanName = party.name.replace(/[^a-zA-Z0-9]/g, '_'); doc.save(`${cleanName}_Ledger.pdf`); };