import XLSX from 'xlsx-js-style'; import { Transaction, Party, BillType } from '../types'; export interface AnalysisExportRow { lotNumber: string; mirchiName: string; purchaseQty: number; purchaseValue: number; avgPurchaseRate: number; soldQty: number; salesValue: number; avgSaleRate: number; realizedProfit: number; marginPercent: number; remainingQty: number; remainingValueAtCost: number; } export interface LotOverviewExportRow { lotNumber: string; mirchiName: string; purchaseQty: number; purchaseValue: number; avgPurchaseRate: number; soldQty: number; salesValue: number; avgSaleRate: number; realizedProfit: number; marginPercent: number; remainingQty: number; remainingValueAtCost: number; } export interface LotDetailExportRow { date: string; billNo: string; typeLabel: string; directionLabel: string; qtyKg: number; ratePerKg: number; grossAmount: number; expensesAllocated: number; netAmount: number; profitImpact: number; } // ========================================== // 2. STYLING CONFIGURATION // ========================================== // Common Colors const CLR_BORDER = { rgb: "D1D5DB" }; // Gray-300 const CLR_TEXT = { rgb: "374151" }; // Gray-700 const CLR_HEAD_BG = { rgb: "F3F4F6" }; // Gray-100 const CLR_GREEN = { rgb: "059669" }; const CLR_RED = { rgb: "DC2626" }; const CLR_TEAL = { rgb: "0D9488" }; const CLR_WHITE = { rgb: "FFFFFF" }; // Border Definitions const BORDER_ALL = { top: { style: 'thin', color: CLR_BORDER }, bottom: { style: 'thin', color: CLR_BORDER }, left: { style: 'thin', color: CLR_BORDER }, right: { style: 'thin', color: CLR_BORDER } }; const BORDER_TOP_SIDE = { top: { style: 'thin', color: { rgb: "9CA3AF" } }, // Darker Gray for Card left: { style: 'thin', color: { rgb: "9CA3AF" } }, right: { style: 'thin', color: { rgb: "9CA3AF" } } }; const BORDER_BOT_SIDE = { bottom: { style: 'thin', color: { rgb: "9CA3AF" } }, left: { style: 'thin', color: { rgb: "9CA3AF" } }, right: { style: 'thin', color: { rgb: "9CA3AF" } } }; // Style Objects const STYLES = { TITLE: { font: { bold: true, sz: 14, color: { rgb: "111827" } }, alignment: { horizontal: 'left', vertical: 'center' } }, // Card Styles CARD_HEADER: { border: BORDER_TOP_SIDE, fill: { fgColor: CLR_WHITE }, font: { sz: 10, color: { rgb: "6B7280" } }, alignment: { vertical: 'bottom', horizontal: 'left', indent: 1 } }, CARD_VALUE: { border: BORDER_BOT_SIDE, fill: { fgColor: CLR_WHITE }, font: { bold: true, sz: 11, color: { rgb: "1F2937" } }, alignment: { vertical: 'top', horizontal: 'left', indent: 1 } }, // Table Styles TABLE_HEAD: { fill: { fgColor: CLR_HEAD_BG }, font: { bold: true, color: CLR_TEXT }, border: BORDER_ALL, alignment: { horizontal: 'center', vertical: 'center' } }, CELL_TEXT: { border: BORDER_ALL, alignment: { vertical: 'center', horizontal: 'left' }, font: { color: CLR_TEXT } }, CELL_NUM: { border: BORDER_ALL, alignment: { vertical: 'center', horizontal: 'right' }, font: { color: { rgb: "111827" } } }, CELL_MONO: { border: BORDER_ALL, alignment: { vertical: 'center', horizontal: 'left' }, font: { name: 'Courier New', sz: 10, color: CLR_TEXT } }, // Text Colors TXT_TEAL: { font: { bold: true, sz: 12, color: CLR_TEAL } }, TXT_GRAY: { font: { color: { rgb: "6B7280" } } }, TXT_GREEN: { font: { bold: true, color: CLR_GREEN } }, TXT_RED: { font: { bold: true, color: CLR_RED } } }; // Formats const FMT_CURRENCY = "₹#,##0.00"; // 2 decimal places for clean alignment const FMT_QTY = "0.00 \"kg\""; // ========================================== // 3. HELPER FUNCTIONS // ========================================== // Helper to create cell. // Uses empty string "" if value is null to avoid "null" text in Excel. const c = (val: any, s: any = {}, fmt?: string) => { return { v: val === null || val === undefined ? "" : val, s: { ...s, numFmt: fmt }, // Inject format into style for xlsx-js-style z: fmt // Inject format into standard key for compatibility }; }; const formatIndCurrency = (val: number) => { return val.toLocaleString('en-IN', { maximumFractionDigits: 0, // Keep summary cards clean (no decimals) style: 'currency', currency: 'INR' }).replace('₹', '₹'); // Ensure symbol consistency }; // ========================================== // 4. EXPORT FUNCTIONS // ========================================== export const exportLotDetailAnalysis = ( lot: LotOverviewExportRow, rows: LotDetailExportRow[], ) => { const data: any[][] = []; // --- ROW 1: Main Title --- data.push([ c(`${lot.lotNumber} - ${lot.mirchiName} Analysis`, STYLES.TITLE), c(""), c(""), c(""), c(""), c(""), c(""), c("") ]); // --- ROW 2: Spacer --- data.push([c("")]); // --- ROW 3: Lot Info (Left) & Remaining (Right) --- // Note: We used c("") for spacers to ensure no "null" text data.push([ c(lot.lotNumber, { ...STYLES.TXT_TEAL, alignment: { vertical: 'bottom' } }), c(""), c(""), c(""), c(""), c(""), c("Remaining", { ...STYLES.TXT_GRAY, alignment: { horizontal: 'right', vertical: 'bottom' } }), c(Number(lot.remainingQty.toFixed(2)), { font: { bold: true }, alignment: { horizontal: 'right', vertical: 'bottom' } }, FMT_QTY) ]); // --- ROW 4: Mirchi Name --- data.push([ c(lot.mirchiName, STYLES.TXT_GRAY), c(""), c(""), c(""), c(""), c(""), c(""), c("") ]); // --- ROW 5: Spacer --- data.push([c("")]); // --- ROW 6: CARD HEADERS --- data.push([ c("Purchase", STYLES.CARD_HEADER), c(""), c(""), c(""), c("Sales", STYLES.CARD_HEADER), c(""), c(""), c("") ]); // --- ROW 7: CARD VALUES --- // Format text manually for the card to ensure it looks perfect const buyTxt = `${lot.purchaseQty.toFixed(2)} kg • ${formatIndCurrency(lot.purchaseValue)}`; const sellTxt = `${lot.soldQty.toFixed(2)} kg • ${formatIndCurrency(lot.salesValue)}`; data.push([ c(buyTxt, STYLES.CARD_VALUE), c(""), c(""), c(""), c(sellTxt, STYLES.CARD_VALUE), c(""), c(""), c("") ]); // --- ROW 8: Spacer --- data.push([c("")]); // --- ROW 9: Avg & Profit Summary --- const avgText = `Avg Buy ₹${lot.avgPurchaseRate.toFixed(0)} /kg • Avg Sell ₹${lot.avgSaleRate.toFixed(0)} /kg`; // Profit Logic const pVal = Number(lot.realizedProfit.toFixed(2)); // Round to avoid .774 const pLabel = pVal > 0 ? "Profit" : pVal < 0 ? "Loss" : "Break-even"; const pStyle = pVal > 0 ? STYLES.TXT_GREEN : pVal < 0 ? STYLES.TXT_RED : STYLES.TXT_GRAY; data.push([ c(avgText, { font: { color: { rgb: "4B5563" }, italic: true }, alignment: { vertical: 'center' } }), c(""), c(""), c(""), c(""), c(""), c(pLabel, { ...STYLES.TXT_GRAY, alignment: { horizontal: 'right', vertical: 'center' } }), c(pVal, { ...pStyle, alignment: { horizontal: 'right', vertical: 'center' } }, FMT_CURRENCY) ]); // --- ROW 10: Spacer --- data.push([c("")]); // --- ROW 11: TABLE HEADERS --- data.push([ c('Date', STYLES.TABLE_HEAD), c('Bill No', STYLES.TABLE_HEAD), c('Type', STYLES.TABLE_HEAD), c('Import / Export', STYLES.TABLE_HEAD), c('Qty (kg)', STYLES.TABLE_HEAD), c('Rate', STYLES.TABLE_HEAD), c('Net Amount', STYLES.TABLE_HEAD), c('Profit Impact', STYLES.TABLE_HEAD) ]); // --- ROW 12+: DATA --- if (rows.length === 0) { data.push([c('No transactions found', STYLES.CELL_TEXT)]); } else { rows.forEach(row => { // Round numbers to 2 decimals to prevent floating point garbage const safeProfit = Number(row.profitImpact.toFixed(2)); const safeNet = Number(row.netAmount.toFixed(2)); const safeQty = Number(row.qtyKg.toFixed(2)); const safeRate = Number(row.ratePerKg.toFixed(2)); const impactStyle = safeProfit > 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_GREEN } } : safeProfit < 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_RED } } : STYLES.CELL_NUM; data.push([ c(row.date, STYLES.CELL_TEXT), c(row.billNo, STYLES.CELL_MONO), c(row.typeLabel, STYLES.CELL_TEXT), c(row.directionLabel, STYLES.CELL_TEXT), c(safeQty, STYLES.CELL_NUM, "0.00"), c(safeRate, STYLES.CELL_NUM, "0.00"), c(safeNet, STYLES.CELL_NUM, FMT_CURRENCY), c(safeProfit, impactStyle, FMT_CURRENCY) ]); }); } // --- GENERATE --- const ws = XLSX.utils.aoa_to_sheet(data); // --- MERGES (0-based indices) --- ws['!merges'] = [ { s: { r: 0, c: 0 }, e: { r: 0, c: 7 } }, // Title { s: { r: 2, c: 0 }, e: { r: 2, c: 5 } }, // Lot Number { s: { r: 3, c: 0 }, e: { r: 3, c: 5 } }, // Mirchi Name // Purchase Card { s: { r: 5, c: 0 }, e: { r: 5, c: 3 } }, { s: { r: 6, c: 0 }, e: { r: 6, c: 3 } }, // Sales Card { s: { r: 5, c: 4 }, e: { r: 5, c: 7 } }, { s: { r: 6, c: 4 }, e: { r: 6, c: 7 } }, // Avg Line { s: { r: 8, c: 0 }, e: { r: 8, c: 5 } }, ]; // --- COL WIDTHS --- ws['!cols'] = [ { wch: 14 }, // Date { wch: 22 }, // Bill No { wch: 12 }, // Type { wch: 14 }, // Imp/Exp { wch: 12 }, // Qty { wch: 10 }, // Rate { wch: 16 }, // Net Amt { wch: 16 }, // Profit ]; const wb = XLSX.utils.book_new(); const safeName = lot.lotNumber.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30); XLSX.utils.book_append_sheet(wb, ws, safeName); XLSX.writeFile(wb, `Lot_${safeName}_Analysis.xlsx`); }; // 5. OTHER EXPORT FUNCTIONS (Parties/Ledger) // ========================================== export const exportPartyLedger = (party: Party, transactions: Transaction[], dateRange?: { from: string; to: string }) => { const parseDate = (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; 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 extractDateFromText = (text?: string): Date | undefined => { if (!text) return undefined; const iso = text.match(/\b\d{4}-\d{2}-\d{2}\b/); if (iso?.[0]) return parseDate(iso[0]); const dmy = text.match(/\b\d{1,2}\/\d{1,2}\/\d{4}\b/); if (dmy?.[0]) return parseDate(dmy[0]); return undefined; }; const isAwaakTx = (tx: Transaction) => { 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'); return isAwaakFromNumber ? true : isJawaakFromNumber ? false : typeStr === 'AWAAK'; }; const inRange = (d: Date) => { if (!dateRange) return true; const from = new Date(dateRange.from); const to = new Date(dateRange.to); return d >= from && d <= to; }; type LedgerEvent = | { kind: 'bill'; date: Date; tx: Transaction } | { kind: 'payment'; date: Date; tx: Transaction; payment: any }; const events: LedgerEvent[] = []; transactions.forEach(tx => { const billDate = parseDate(tx.bill_date) || new Date(tx.bill_date); if (!inRange(billDate)) return; events.push({ kind: 'bill', date: billDate, tx }); const payments = tx.payments || []; const validPayments = payments.filter((p: any) => String(p.mode).toLowerCase() !== 'due'); validPayments.forEach((p: any) => { const paymentDate = parseDate(p.payment_date) || parseDate(p.created_at) || parseDate(p.date) || extractDateFromText(p.reference) || billDate; const safePaymentDate = paymentDate < billDate ? billDate : paymentDate; if (!inRange(safePaymentDate)) return; events.push({ kind: 'payment', date: safePaymentDate, tx, payment: p }); }); }); events.sort((a, b) => { const ta = a.date.getTime(); const tb = b.date.getTime(); if (ta !== tb) return ta - tb; if (a.kind !== b.kind) return a.kind === 'bill' ? -1 : 1; return (a.tx.bill_number || '').localeCompare(b.tx.bill_number || ''); }); const data: any[][] = []; data.push([c(`Ledger: ${party.name}`, STYLES.TITLE)]); data.push([c("")]); data.push(['Date', 'Particulars', 'Credit', 'Debit', 'Balance', 'Dr/Cr'].map(h => c(h, STYLES.TABLE_HEAD))); let running = 0, tCr = 0, tDr = 0; events.forEach(ev => { const tx = ev.tx; const isAwaak = isAwaakTx(tx); let cr = 0, dr = 0; if (ev.kind === 'bill') { if (isAwaak) { if (tx.is_return) dr = tx.total_amount; else cr = tx.total_amount; } else { if (tx.is_return) cr = tx.total_amount; else dr = tx.total_amount; } const desc = `${tx.bill_number}` + (tx.is_return ? ' (RETURN)' : '') + (tx.items.length ? ` - ${tx.items[0].mirchi_name}` : ""); tCr += cr; tDr += dr; running += (dr - cr); data.push([ c(ev.date.toLocaleDateString('en-IN'), STYLES.CELL_TEXT), c(desc, STYLES.CELL_TEXT), c(cr || "", STYLES.CELL_NUM, FMT_CURRENCY), c(dr || "", STYLES.CELL_NUM, FMT_CURRENCY), c(Math.abs(running), STYLES.CELL_NUM, FMT_CURRENCY), c(running >= 0 ? 'Dr' : 'Cr', STYLES.CELL_TEXT) ]); return; } const p = ev.payment; const amount = Number(p.amount) || 0; if (isAwaak) { if (tx.is_return) cr = amount; else dr = amount; } else { if (tx.is_return) dr = amount; else cr = amount; } const modeStr = String(p.mode || '').charAt(0).toUpperCase() + String(p.mode || '').slice(1); let desc = ` [${modeStr}]`; if (p.reference) desc += ` ${p.reference}`; tCr += cr; tDr += dr; running += (dr - cr); data.push([ c(ev.date.toLocaleDateString('en-IN'), STYLES.CELL_TEXT), c(desc, STYLES.CELL_TEXT), c(cr || "", STYLES.CELL_NUM, FMT_CURRENCY), c(dr || "", STYLES.CELL_NUM, FMT_CURRENCY), c(Math.abs(running), STYLES.CELL_NUM, FMT_CURRENCY), c(running >= 0 ? 'Dr' : 'Cr', STYLES.CELL_TEXT) ]); }); const totStyle = { ...STYLES.CELL_NUM, font: { bold: true } }; data.push([ c('TOTAL', { ...STYLES.CELL_TEXT, font: { bold: true } }), c(""), c(tCr, totStyle, FMT_CURRENCY), c(tDr, totStyle, FMT_CURRENCY), c(Math.abs(running), totStyle, FMT_CURRENCY), c(running > 0 ? 'Dr' : 'Cr', { ...STYLES.CELL_TEXT, font: { bold: true } }) ]); const ws = XLSX.utils.aoa_to_sheet(data); ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 5 } }]; ws['!cols'] = [{ wch: 12 }, { wch: 40 }, { wch: 15 }, { wch: 15 }, { wch: 15 }, { wch: 8 }]; const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Ledger'); XLSX.writeFile(wb, `${party.name.replace(/\s/g,'_')}_Ledger.xlsx`); }; export const exportAllParties = (parties: Party[]) => { const data: any[][] = []; data.push(['Party Name', 'Phone', 'City', 'Type', 'Balance', 'Status'].map(h => c(h, STYLES.TABLE_HEAD))); let tRec = 0, tPay = 0; parties.forEach(p => { if(p.current_balance > 0) tRec += p.current_balance; if(p.current_balance < 0) tPay += Math.abs(p.current_balance); const color = p.current_balance > 0 ? CLR_GREEN : p.current_balance < 0 ? CLR_RED : undefined; data.push([ c(p.name, STYLES.CELL_TEXT), c(p.phone||'-', STYLES.CELL_TEXT), c(p.city||'-', STYLES.CELL_TEXT), c(p.party_type, STYLES.CELL_TEXT), c(p.current_balance, { ...STYLES.CELL_NUM, font: { color } }, FMT_CURRENCY), c(p.current_balance > 0 ? 'Rec' : p.current_balance < 0 ? 'Pay' : '-', STYLES.CELL_TEXT) ]); }); data.push([c("")]); data.push([c('SUMMARY', { font: { bold: true } })]); data.push([c('Total Receivable'), c(""), c(""), c(""), c(tRec, STYLES.CELL_NUM, FMT_CURRENCY)]); data.push([c('Total Payable'), c(""), c(""), c(""), c(tPay, STYLES.CELL_NUM, FMT_CURRENCY)]); const ws = XLSX.utils.aoa_to_sheet(data); ws['!cols'] = [{ wch: 30 }, { wch: 15 }, { wch: 20 }, { wch: 10 }, { wch: 18 }, { wch: 10 }]; const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Parties'); XLSX.writeFile(wb, `All_Parties.xlsx`); }; export const exportAllTransactions = (transactions: Transaction[]) => { const data: any[][] = []; data.push(['Date', 'Bill No', 'Party', 'Type', 'Amount', 'Paid', 'Balance'].map(h => c(h, STYLES.TABLE_HEAD))); transactions.forEach(tx => { data.push([ c(new Date(tx.bill_date).toLocaleDateString('en-IN'), STYLES.CELL_TEXT), c(tx.bill_number, STYLES.CELL_MONO), c(tx.party_name, STYLES.CELL_TEXT), c(tx.bill_type, STYLES.CELL_TEXT), c(tx.total_amount, STYLES.CELL_NUM, FMT_CURRENCY), c(tx.paid_amount, STYLES.CELL_NUM, FMT_CURRENCY), c(tx.balance_amount, STYLES.CELL_NUM, FMT_CURRENCY) ]); }); const ws = XLSX.utils.aoa_to_sheet(data); ws['!cols'] = [{ wch: 12 }, { wch: 20 }, { wch: 25 }, { wch: 10 }, { wch: 15 }, { wch: 15 }, { wch: 15 }]; const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Transactions'); XLSX.writeFile(wb, `All_Transactions.xlsx`); }; export const exportAnalysisReport = (rows: AnalysisExportRow[]) => { if (!rows.length) return; const data: any[][] = []; const headers = ['LOT','Mirchi','Buy Qty','Buy Val','Avg Buy','Sell Qty','Sell Val','Avg Sell','Rem Qty','Rem Val','Profit','Margin %']; data.push(headers.map(h => c(h, STYLES.TABLE_HEAD))); rows.forEach(r => { const pStyle = r.realizedProfit > 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_GREEN } } : r.realizedProfit < 0 ? { ...STYLES.CELL_NUM, font: { color: CLR_RED } } : STYLES.CELL_NUM; data.push([ c(r.lotNumber, STYLES.CELL_TEXT), c(r.mirchiName, STYLES.CELL_TEXT), c(r.purchaseQty, STYLES.CELL_NUM,"0.00"), c(r.purchaseValue, STYLES.CELL_NUM,FMT_CURRENCY), c(r.avgPurchaseRate, STYLES.CELL_NUM,FMT_CURRENCY), c(r.soldQty, STYLES.CELL_NUM,"0.00"), c(r.salesValue, STYLES.CELL_NUM,FMT_CURRENCY), c(r.avgSaleRate, STYLES.CELL_NUM,FMT_CURRENCY), c(r.remainingQty, STYLES.CELL_NUM,"0.00"), c(r.remainingValueAtCost, STYLES.CELL_NUM,FMT_CURRENCY), c(r.realizedProfit, pStyle,FMT_CURRENCY), c(r.marginPercent/100, STYLES.CELL_NUM,"0.00%") ]); }); const ws = XLSX.utils.aoa_to_sheet(data); ws['!cols'] = headers.map(() => ({ wch: 14 })); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Summary'); XLSX.writeFile(wb, `Analysis_Summary.xlsx`); };