stagingfrontend / utils /LedgerPdfGenerator.ts
Antaram's picture
Upload 40 files
c2ddef6 verified
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`);
};