update detail wallet statement

This commit is contained in:
Raja Oktafrianto
2025-06-06 11:57:57 +07:00
parent bf4a67b64c
commit 3fa9764340

View File

@ -16,8 +16,14 @@ import { format } from 'date-fns';
import FilterSection from './FilterSection';
import * as XLSX from 'xlsx';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import ExcelJS from 'exceljs';
let autoTable: any;
try {
// Method 1: Default import
autoTable = require('jspdf-autotable').default;
} catch (e1) {
console.error('Failed to import jspdf-autotable:', e1);
}
interface ContextProps {
doExportData: (sorting: any, filter: any) => Promise<any>;
@ -227,6 +233,15 @@ const ShowDetailWalletDialog = () => {
}));
};
const isImageAccessible = async (url: any) => {
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch {
return false;
}
};
const doExportExcel = async () => {
try {
const allData = await getAllTransactionData();
@ -235,11 +250,9 @@ const ShowDetailWalletDialog = () => {
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet([]);
// Membuat space untuk header image (sekitar 10 baris)
// Space untuk header image
const imageSpaceRows = 10;
const companyHeader = [
// Baris kosong untuk space gambar header
...Array(imageSpaceRows).fill(['']),
[''],
['Wallet Statement Details'],
@ -250,17 +263,8 @@ const ShowDetailWalletDialog = () => {
const columnHeaders = [
[
'Transaction Code',
'MSISDN Reffer',
'Transaction Type',
'Type',
'Pre Amount',
'Amount',
'Post Amount',
'Category',
'Date',
'Purpose',
'Notes'
'Transaction Code', 'MSISDN Reffer', 'Transaction Type', 'Type',
'Pre Amount', 'Amount', 'Post Amount', 'Category', 'Date', 'Purpose', 'Notes'
]
];
@ -274,21 +278,12 @@ const ShowDetailWalletDialog = () => {
// Set column widths
const colWidths = [
{ wch: 20 },
{ wch: 15 },
{ wch: 35 },
{ wch: 10 },
{ wch: 12 },
{ wch: 12 },
{ wch: 12 },
{ wch: 15 },
{ wch: 20 },
{ wch: 25 },
{ wch: 25 }
{ wch: 20 }, { wch: 15 }, { wch: 35 }, { wch: 10 }, { wch: 12 },
{ wch: 12 }, { wch: 12 }, { wch: 15 }, { wch: 20 }, { wch: 25 }, { wch: 25 }
];
ws['!cols'] = colWidths;
// Style untuk title "Wallet Statement Details"
// Apply styles
const titleRowIndex = imageSpaceRows + 1;
const titleCell = XLSX.utils.encode_cell({ r: titleRowIndex, c: 0 });
if (ws[titleCell]) {
@ -298,33 +293,19 @@ const ShowDetailWalletDialog = () => {
};
}
// Style untuk export date dan wallet ID
for (let i = titleRowIndex + 1; i < titleRowIndex + 3; i++) {
const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 });
if (ws[cellRef]) {
ws[cellRef].s = {
font: { sz: 11 },
alignment: { horizontal: 'left' }
};
}
}
// Merge cells untuk header area
// Merge cells
if (!ws['!merges']) ws['!merges'] = [];
// Merge untuk space gambar header
for (let i = 0; i < imageSpaceRows; i++) {
const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } });
ws['!merges'].push(XLSX.utils.decode_range(range));
}
// Merge untuk title dan info
for (let i = titleRowIndex; i < titleRowIndex + 4; i++) {
const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } });
ws['!merges'].push(XLSX.utils.decode_range(range));
}
// Style untuk column headers
// Style column headers
const headerRowIndex = titleRowIndex + 4;
for (let col = 0; col < columnHeaders[0].length; col++) {
const cellRef = XLSX.utils.encode_cell({ r: headerRowIndex, c: col });
@ -343,14 +324,14 @@ const ShowDetailWalletDialog = () => {
}
}
// Style untuk data rows
const dataStartRow = headerRowIndex + 1;
// Style data rows
const dataStartRow = headerRowIndex + 1;
for (let row = dataStartRow; row < dataStartRow + formattedData.length; row++) {
for (let col = 0; col < columnHeaders[0].length; col++) {
const cellRef = XLSX.utils.encode_cell({ r: row, c: col });
if (ws[cellRef]) {
ws[cellRef].s = {
alignment: { horizontal: 'left' }, // Semua kolom rata kiri
alignment: { horizontal: 'left' },
border: {
top: { style: 'thin', color: { rgb: 'D9D9D9' } },
bottom: { style: 'thin', color: { rgb: 'D9D9D9' } },
@ -362,121 +343,165 @@ const ShowDetailWalletDialog = () => {
}
}
// Try ExcelJS with better error handling
let useExcelJS = false;
try {
// Import ExcelJS untuk menambahkan gambar
const ExcelJS = await import('exceljs');
// Check if ExcelJS is available
const ExcelJSModule = await import('exceljs');
const ExcelJS = ExcelJSModule.default || ExcelJSModule;
// Buat workbook baru dengan ExcelJS
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Wallet Statement');
if (ExcelJS && ExcelJS.Workbook) {
useExcelJS = true;
// Tambahkan data dari XLSX worksheet ke ExcelJS worksheet
const range = XLSX.utils.decode_range(ws['!ref'] || 'A1:K1');
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Wallet Statement');
for (let row = range.s.r; row <= range.e.r; row++) {
for (let col = range.s.c; col <= range.e.c; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
const cell = ws[cellAddress];
if (cell && cell.v !== undefined) {
const excelCell = worksheet.getCell(row + 1, col + 1);
excelCell.value = cell.v;
// Transfer data from XLSX to ExcelJS
const range = XLSX.utils.decode_range(ws['!ref'] || 'A1:K1');
for (let row = range.s.r; row <= range.e.r; row++) {
for (let col = range.s.c; col <= range.e.c; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
const cell = ws[cellAddress];
if (cell && cell.v !== undefined) {
const excelCell = worksheet.getCell(row + 1, col + 1);
excelCell.value = cell.v;
// Apply styles
if (cell.s) {
if (cell.s.font) {
excelCell.font = {
bold: cell.s.font.bold || false,
size: cell.s.font.sz || 11,
color: cell.s.font.color ? { argb: cell.s.font.color.rgb } : undefined
};
}
if (cell.s.fill) {
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: cell.s.fill.fgColor.rgb }
};
}
if (cell.s.alignment) {
excelCell.alignment = {
horizontal: cell.s.alignment.horizontal || 'left'
};
}
if (cell.s.border) {
excelCell.border = {
top: cell.s.border.top ? { style: 'thin' } : undefined,
bottom: cell.s.border.bottom ? { style: 'thin' } : undefined,
left: cell.s.border.left ? { style: 'thin' } : undefined,
right: cell.s.border.right ? { style: 'thin' } : undefined
};
// Apply styles
if (cell.s) {
if (cell.s.font) {
excelCell.font = {
bold: cell.s.font.bold || false,
size: cell.s.font.sz || 11,
color: cell.s.font.color ? { argb: cell.s.font.color.rgb } : undefined
};
}
if (cell.s.fill) {
excelCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: cell.s.fill.fgColor.rgb }
};
}
if (cell.s.alignment) {
excelCell.alignment = {
horizontal: cell.s.alignment.horizontal || 'left'
};
}
if (cell.s.border) {
excelCell.border = {
top: cell.s.border.top ? { style: 'thin' } : undefined,
bottom: cell.s.border.bottom ? { style: 'thin' } : undefined,
left: cell.s.border.left ? { style: 'thin' } : undefined,
right: cell.s.border.right ? { style: 'thin' } : undefined
};
}
}
}
}
}
}
// Set column widths
colWidths.forEach((width, index) => {
worksheet.getColumn(index + 1).width = width.wch;
});
// Merge cells
if (ws['!merges']) {
ws['!merges'].forEach((merge) => {
worksheet.mergeCells(merge.s.r + 1, merge.s.c + 1, merge.e.r + 1, merge.e.c + 1);
// Set column widths
colWidths.forEach((width, index) => {
worksheet.getColumn(index + 1).width = width.wch;
});
}
// Tambahkan gambar header
try {
const response = await fetch('/media/avatars/KopDetailWalletStatement.png');
if (response.ok) {
const imageBuffer = await response.arrayBuffer();
const imageId = workbook.addImage({
buffer: imageBuffer,
extension: 'png'
});
// Posisikan gambar di area header
worksheet.addImage(imageId, {
tl: { col: 1.7, row: 1 },
ext: { width: 1100, height: 180 }
// Merge cells
if (ws['!merges']) {
ws['!merges'].forEach((merge) => {
worksheet.mergeCells(merge.s.r + 1, merge.s.c + 1, merge.e.r + 1, merge.e.c + 1);
});
}
} catch (imageError) {
console.warn('Could not load header image, continuing without it:', imageError);
// Try to add header image dengan multiple fallback paths
const imagePaths = [
'/media/avatars/KopDetailWalletStatement.png',
'./media/avatars/KopDetailWalletStatement.png',
];
let imageAdded = false;
for (const imagePath of imagePaths) {
try {
if (await isImageAccessible(imagePath)) {
const response = await fetch(imagePath);
if (response.ok) {
const imageBuffer = await response.arrayBuffer();
const imageId = workbook.addImage({
buffer: imageBuffer,
extension: 'png'
});
worksheet.addImage(imageId, {
tl: { col: 1.7, row: 1 },
ext: { width: 1100, height: 180 }
});
imageAdded = true;
break;
}
}
} catch (imageError) {
console.warn(`Failed to load image from ${imagePath}:`, imageError);
continue;
}
}
if (!imageAdded) {
console.warn('No header image could be loaded from any path');
}
// Export file
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
window.URL.revokeObjectURL(url);
}
// Export file
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
const buffer = await workbook.xlsx.writeBuffer();
// Download file
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.click();
window.URL.revokeObjectURL(url);
} catch (excelJSError) {
console.warn('ExcelJS not available, falling back to XLSX without image:', excelJSError);
console.warn('ExcelJS failed, falling back to XLSX:', excelJSError);
useExcelJS = false;
}
// Fallback to XLSX if ExcelJS failed
if (!useExcelJS) {
XLSX.utils.book_append_sheet(wb, ws, 'Wallet Statement');
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
XLSX.writeFile(wb, fileName);
}
} catch (error) {
toast.success('Excel export completed successfully');
} catch (error: any) {
console.error('Error exporting to Excel:', error);
toast.error('Failed to export Excel');
toast.error('Failed to export Excel: ' + error.message);
}
};
const doExportPDF = async () => {
try {
// Check if autoTable is available
if (!autoTable) {
// Try to load autoTable dynamically
try {
const autoTableModule = await import('jspdf-autotable');
autoTable = autoTableModule.default || autoTableModule.autoTable || autoTableModule;
} catch (importError) {
console.error('Failed to import jspdf-autotable:', importError);
toast.error('PDF export library not available');
return;
}
}
if (typeof autoTable !== 'function') {
console.error('autoTable is not a function, type:', typeof autoTable);
toast.error('PDF export function not properly loaded');
return;
}
const allData = await getAllTransactionData();
const formattedData = formatTransactionData(allData);
@ -486,92 +511,84 @@ const ShowDetailWalletDialog = () => {
format: 'a4'
});
const headerImageUrl = '/media/avatars/KopDetailWalletStatement.png';
// Try to add header image dengan multiple paths
const imagePaths = [
'/media/avatars/KopDetailWalletStatement.png',
'./media/avatars/KopDetailWalletStatement.png',
];
let imageAdded = false;
for (const imagePath of imagePaths) {
try {
if (await isImageAccessible(imagePath)) {
const pageWidth = pdf.internal.pageSize.getWidth();
const imageWidth = 250;
const imageHeight = 40;
const xPosition = (pageWidth - imageWidth) / 2;
pdf.addImage(imagePath, 'PNG', xPosition, 10, imageWidth, imageHeight);
imageAdded = true;
break;
}
} catch (imageError) {
console.warn(`Failed to load image from ${imagePath}:`, imageError);
continue;
}
}
const pageWidth = pdf.internal.pageSize.getWidth();
let currentY = imageAdded ? 55 : 20;
// Add header line
pdf.setDrawColor(0, 0, 0);
pdf.setLineWidth(0.5);
pdf.line(20, currentY, pageWidth - 20, currentY);
currentY += 10;
// Add title
pdf.setFontSize(16);
pdf.setFont('helvetica', 'bold');
pdf.text('Wallet Statement Details', pageWidth / 2, currentY, { align: 'center' });
currentY += 10;
// Add metadata
pdf.setFontSize(10);
pdf.setFont('helvetica', 'normal');
pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, currentY);
pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, currentY + 6);
currentY += 20;
// Prepare table data
const tableHeaders = [
'Transaction Code', 'MSISDN Reffer', 'Transaction Type', 'Type',
'Pre Amount', 'Amount', 'Post Amount', 'Category', 'Date', 'Purpose', 'Notes'
];
const tableData = formattedData.map((item) => [
item['Transaction Code'], item['MSISDN Reffer'], item['Transaction Type'], item['Type'],
item['Pre Amount'], item['Amount'], item['Post Amount'], item['Category'],
item['Date'], item['Purpose'], item['Notes']
]);
const columnStyles = {
0: { cellWidth: 25, halign: 'left' },
1: { cellWidth: 20, halign: 'left' },
2: { cellWidth: 25, halign: 'left' },
3: { cellWidth: 15, halign: 'center' },
4: { cellWidth: 18, halign: 'right' },
5: { cellWidth: 18, halign: 'right' },
6: { cellWidth: 18, halign: 'right' },
7: { cellWidth: 20, halign: 'left' },
8: { cellWidth: 30, halign: 'left' },
9: { cellWidth: 25, halign: 'left' },
10: { cellWidth: 25, halign: 'left' }
};
const totalTableWidth = Object.values(columnStyles).reduce((sum, col) => sum + col.cellWidth, 0);
const leftMargin = (pageWidth - totalTableWidth) / 2;
// Use autoTable with proper error handling
try {
const pageWidth = pdf.internal.pageSize.getWidth();
const imageWidth = 250;
const imageHeight = 40;
const xPosition = (pageWidth - imageWidth) / 2;
pdf.addImage(headerImageUrl, 'PNG', xPosition, 10, imageWidth, imageHeight);
let currentY = 55;
pdf.setDrawColor(0, 0, 0);
pdf.setLineWidth(0.5);
pdf.line(20, currentY, pageWidth - 20, currentY);
currentY += 10;
pdf.setFontSize(16);
pdf.setFont('helvetica', 'bold');
pdf.text('Wallet Statement Details', pageWidth / 2, currentY, { align: 'center' });
currentY += 10;
pdf.setFontSize(10);
pdf.setFont('helvetica', 'normal');
pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, currentY);
pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, currentY + 6);
currentY += 20;
const tableHeaders = [
'Transaction Code',
'MSISDN Reffer',
'Transaction Type',
'Type',
'Pre Amount',
'Amount',
'Post Amount',
'Category',
'Date',
'Purpose',
'Notes'
];
const tableData = formattedData.map((item) => [
item['Transaction Code'],
item['MSISDN Reffer'],
item['Transaction Type'],
item['Type'],
item['Pre Amount'],
item['Amount'],
item['Post Amount'],
item['Category'],
item['Date'],
item['Purpose'],
item['Notes']
]);
const calculateLeftMargin = (
columnStyles: Record<number, { cellWidth: number }>,
pageWidth: number
): number => {
const totalTableWidth = Object.values(columnStyles).reduce(
(sum, col) => sum + col.cellWidth,
0
);
return (pageWidth - totalTableWidth) / 2;
};
const columnStyles = {
0: { cellWidth: 25, halign: 'left' as const },
1: { cellWidth: 20, halign: 'left' as const },
2: { cellWidth: 25, halign: 'left' as const },
3: { cellWidth: 15, halign: 'center' as const },
4: { cellWidth: 18, halign: 'right' as const },
5: { cellWidth: 18, halign: 'right' as const },
6: { cellWidth: 18, halign: 'right' as const },
7: { cellWidth: 20, halign: 'left' as const },
8: { cellWidth: 30, halign: 'left' as const },
9: { cellWidth: 25, halign: 'left' as const },
10: { cellWidth: 25, halign: 'left' as const }
};
const leftMargin = calculateLeftMargin(columnStyles, pageWidth);
autoTable(pdf, {
head: [tableHeaders],
body: tableData,
@ -598,15 +615,19 @@ const ShowDetailWalletDialog = () => {
}
}
});
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`;
pdf.save(fileName);
} catch (imageError) {
console.error('Error loading header image:', imageError);
} catch (autoTableError) {
console.error('AutoTable error:', autoTableError);
toast.error('Failed to generate PDF table');
return;
}
} catch (error) {
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`;
pdf.save(fileName);
toast.success('PDF export completed successfully');
} catch (error: any) {
console.error('Error exporting to PDF:', error);
toast.error('Failed to export PDF');
toast.error('Failed to export PDF: ' + error.message);
}
};