update export detail wallet statement

This commit is contained in:
Raja Oktafrianto
2025-06-06 10:02:30 +07:00
parent 3e5f7948f0
commit bf4a67b64c
2 changed files with 264 additions and 170 deletions

View File

@ -50,6 +50,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.4", "cmdk": "^1.0.4",
"date-fns": "^3.0.0", "date-fns": "^3.0.0",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"formik": "^2.4.6", "formik": "^2.4.6",
"helmet": "^8.1.0", "helmet": "^8.1.0",

View File

@ -16,13 +16,8 @@ import { format } from 'date-fns';
import FilterSection from './FilterSection'; import FilterSection from './FilterSection';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import 'jspdf-autotable'; import autoTable from 'jspdf-autotable';
import ExcelJS from 'exceljs';
interface AutoTableData {
pageNumber: number;
table: any;
doc: jsPDF;
}
interface ContextProps { interface ContextProps {
doExportData: (sorting: any, filter: any) => Promise<any>; doExportData: (sorting: any, filter: any) => Promise<any>;
@ -238,15 +233,14 @@ const ShowDetailWalletDialog = () => {
const formattedData = formatTransactionData(allData); const formattedData = formatTransactionData(allData);
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet([]); const ws = XLSX.utils.json_to_sheet([]);
// Membuat space untuk header image (sekitar 10 baris)
const imageSpaceRows = 10;
const companyHeader = [ const companyHeader = [
['T•PAY'], // Baris kosong untuk space gambar header
['Telin Digital Solution'], ...Array(imageSpaceRows).fill(['']),
['Av. Pres. Nicolau Lobato, Dili'],
['Tel: (+670) 74147147 / 147'],
['www.t-pay.tl'],
[''], [''],
['Wallet Statement Details'], ['Wallet Statement Details'],
[`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`], [`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`],
@ -278,10 +272,11 @@ const ShowDetailWalletDialog = () => {
XLSX.utils.sheet_add_aoa(ws, excelData); XLSX.utils.sheet_add_aoa(ws, excelData);
// Set column widths
const colWidths = [ const colWidths = [
{ wch: 20 }, { wch: 20 },
{ wch: 15 }, { wch: 15 },
{ wch: 25 }, { wch: 35 },
{ wch: 10 }, { wch: 10 },
{ wch: 12 }, { wch: 12 },
{ wch: 12 }, { wch: 12 },
@ -293,35 +288,18 @@ const ShowDetailWalletDialog = () => {
]; ];
ws['!cols'] = colWidths; ws['!cols'] = colWidths;
const tpayCell = XLSX.utils.encode_cell({ r: 0, c: 0 }); // Style untuk title "Wallet Statement Details"
if (ws[tpayCell]) { const titleRowIndex = imageSpaceRows + 1;
ws[tpayCell].s = { const titleCell = XLSX.utils.encode_cell({ r: titleRowIndex, c: 0 });
font: { bold: true, sz: 18, color: { rgb: '000000' } },
alignment: { horizontal: 'center' },
fill: { fgColor: { rgb: 'F4C2C2' } }
};
}
for (let i = 1; i < 5; i++) {
const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 });
if (ws[cellRef]) {
ws[cellRef].s = {
font: { sz: 12, color: { rgb: '000000' } },
alignment: { horizontal: 'center' },
fill: { fgColor: { rgb: 'F4C2C2' } }
};
}
}
const titleCell = XLSX.utils.encode_cell({ r: 6, c: 0 });
if (ws[titleCell]) { if (ws[titleCell]) {
ws[titleCell].s = { ws[titleCell].s = {
font: { bold: true, sz: 14 }, font: { bold: true, sz: 16 },
alignment: { horizontal: 'center' } alignment: { horizontal: 'center' }
}; };
} }
for (let i = 7; i < 9; i++) { // 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 }); const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 });
if (ws[cellRef]) { if (ws[cellRef]) {
ws[cellRef].s = { ws[cellRef].s = {
@ -331,14 +309,25 @@ const ShowDetailWalletDialog = () => {
} }
} }
for (let i = 0; i < 6; i++) { // Merge cells untuk header area
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 } }); const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } });
if (!ws['!merges']) ws['!merges'] = [];
ws['!merges'].push(XLSX.utils.decode_range(range)); 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
const headerRowIndex = titleRowIndex + 4;
for (let col = 0; col < columnHeaders[0].length; col++) { for (let col = 0; col < columnHeaders[0].length; col++) {
const cellRef = XLSX.utils.encode_cell({ r: 10, c: col }); const cellRef = XLSX.utils.encode_cell({ r: headerRowIndex, c: col });
if (ws[cellRef]) { if (ws[cellRef]) {
ws[cellRef].s = { ws[cellRef].s = {
font: { bold: true, color: { rgb: 'FFFFFF' } }, font: { bold: true, color: { rgb: 'FFFFFF' } },
@ -354,11 +343,14 @@ const ShowDetailWalletDialog = () => {
} }
} }
for (let row = 11; row < 11 + formattedData.length; row++) { // Style untuk data rows
const dataStartRow = headerRowIndex + 1;
for (let row = dataStartRow; row < dataStartRow + formattedData.length; row++) {
for (let col = 0; col < columnHeaders[0].length; col++) { for (let col = 0; col < columnHeaders[0].length; col++) {
const cellRef = XLSX.utils.encode_cell({ r: row, c: col }); const cellRef = XLSX.utils.encode_cell({ r: row, c: col });
if (ws[cellRef]) { if (ws[cellRef]) {
ws[cellRef].s = { ws[cellRef].s = {
alignment: { horizontal: 'left' }, // Semua kolom rata kiri
border: { border: {
top: { style: 'thin', color: { rgb: 'D9D9D9' } }, top: { style: 'thin', color: { rgb: 'D9D9D9' } },
bottom: { style: 'thin', color: { rgb: 'D9D9D9' } }, bottom: { style: 'thin', color: { rgb: 'D9D9D9' } },
@ -366,156 +358,257 @@ const ShowDetailWalletDialog = () => {
right: { style: 'thin', color: { rgb: 'D9D9D9' } } right: { style: 'thin', color: { rgb: 'D9D9D9' } }
} }
}; };
if ([4, 5, 6].includes(col)) {
ws[cellRef].s.alignment = { horizontal: 'right' };
}
} }
} }
} }
XLSX.utils.book_append_sheet(wb, ws, 'Wallet Statement'); try {
// Import ExcelJS untuk menambahkan gambar
const ExcelJS = await import('exceljs');
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; // Buat workbook baru dengan ExcelJS
XLSX.writeFile(wb, fileName); const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Wallet Statement');
// console.log('Excel export completed successfully'); // Tambahkan data dari XLSX worksheet ke ExcelJS worksheet
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
};
}
}
}
}
}
// 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);
});
}
// 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 }
});
}
} catch (imageError) {
console.warn('Could not load header image, continuing without it:', imageError);
}
// 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);
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) { } catch (error) {
console.error('Error exporting to Excel:', error); console.error('Error exporting to Excel:', error);
toast.error('Failed to export Excel');
} }
}; };
// Perbaikan untuk fungsi doExportPDF const doExportPDF = async () => {
const doExportPDF = async () => { try {
try { const allData = await getAllTransactionData();
// Import jsPDF dengan cara yang benar const formattedData = formatTransactionData(allData);
const jsPDF = (await import('jspdf')).default;
// Import autoTable plugin const pdf = new jsPDF({
const autoTable = (await import('jspdf-autotable')).default; orientation: 'landscape',
unit: 'mm',
format: 'a4'
});
const allData = await getAllTransactionData(); const headerImageUrl = '/media/avatars/KopDetailWalletStatement.png';
const formattedData = formatTransactionData(allData);
// Buat instance jsPDF try {
const pdf = new jsPDF({ const pageWidth = pdf.internal.pageSize.getWidth();
orientation: 'landscape', // Ubah ke landscape untuk tabel yang lebar const imageWidth = 250;
unit: 'mm', const imageHeight = 40;
format: 'a4' const xPosition = (pageWidth - imageWidth) / 2;
});
// Header perusahaan pdf.addImage(headerImageUrl, 'PNG', xPosition, 10, imageWidth, imageHeight);
pdf.setFontSize(18);
pdf.setFont('helvetica', 'bold');
pdf.text('T•PAY', pdf.internal.pageSize.getWidth() / 2, 20, { align: 'center' });
pdf.setFontSize(12); let currentY = 55;
pdf.setFont('helvetica', 'normal');
pdf.text('Telin Digital Solution', pdf.internal.pageSize.getWidth() / 2, 28, { align: 'center' });
pdf.text('Av. Pres. Nicolau Lobato, Dili', pdf.internal.pageSize.getWidth() / 2, 34, { align: 'center' });
pdf.text('Tel: (+670) 74147147 / 147', pdf.internal.pageSize.getWidth() / 2, 40, { align: 'center' });
pdf.text('www.t-pay.tl', pdf.internal.pageSize.getWidth() / 2, 46, { align: 'center' });
// Garis pemisah pdf.setDrawColor(0, 0, 0);
pdf.setDrawColor(0, 0, 0); pdf.setLineWidth(0.5);
pdf.setLineWidth(0.5); pdf.line(20, currentY, pageWidth - 20, currentY);
pdf.line(20, 52, pdf.internal.pageSize.getWidth() - 20, 52);
// Judul laporan currentY += 10;
pdf.setFontSize(16);
pdf.setFont('helvetica', 'bold');
pdf.text('Wallet Statement Details', pdf.internal.pageSize.getWidth() / 2, 62, { align: 'center' });
// Informasi tambahan pdf.setFontSize(16);
pdf.setFontSize(10); pdf.setFont('helvetica', 'bold');
pdf.setFont('helvetica', 'normal'); pdf.text('Wallet Statement Details', pageWidth / 2, currentY, { align: 'center' });
pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, 72);
pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, 78);
// Siapkan data tabel currentY += 10;
const tableHeaders = [
'Transaction Code',
'MSISDN Reffer',
'Transaction Type',
'Type',
'Pre Amount',
'Amount',
'Post Amount',
'Category',
'Date',
'Purpose',
'Notes'
];
const tableData = formattedData.map((item) => [ pdf.setFontSize(10);
item['Transaction Code'], pdf.setFont('helvetica', 'normal');
item['MSISDN Reffer'], pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, currentY);
item['Transaction Type'], pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, currentY + 6);
item['Type'],
item['Pre Amount'],
item['Amount'],
item['Post Amount'],
item['Category'],
item['Date'],
item['Purpose'],
item['Notes']
]);
// Gunakan autoTable dengan cara yang benar currentY += 20;
autoTable(pdf, {
head: [tableHeaders], const tableHeaders = [
body: tableData, 'Transaction Code',
startY: 85, 'MSISDN Reffer',
styles: { 'Transaction Type',
fontSize: 7, 'Type',
cellPadding: 2, 'Pre Amount',
overflow: 'linebreak', 'Amount',
halign: 'center' 'Post Amount',
}, 'Category',
headStyles: { 'Date',
fillColor: [68, 114, 196], 'Purpose',
textColor: [255, 255, 255], 'Notes'
fontStyle: 'bold', ];
halign: 'center'
}, const tableData = formattedData.map((item) => [
columnStyles: { item['Transaction Code'],
0: { cellWidth: 25, halign: 'left' }, // Transaction Code item['MSISDN Reffer'],
1: { cellWidth: 20, halign: 'left' }, // MSISDN Reffer item['Transaction Type'],
2: { cellWidth: 25, halign: 'left' }, // Transaction Type item['Type'],
3: { cellWidth: 15, halign: 'center' }, // Type item['Pre Amount'],
4: { cellWidth: 18, halign: 'right' }, // Pre Amount item['Amount'],
5: { cellWidth: 18, halign: 'right' }, // Amount item['Post Amount'],
6: { cellWidth: 18, halign: 'right' }, // Post Amount item['Category'],
7: { cellWidth: 20, halign: 'left' }, // Category item['Date'],
8: { cellWidth: 30, halign: 'left' }, // Date item['Purpose'],
9: { cellWidth: 25, halign: 'left' }, // Purpose item['Notes']
10: { cellWidth: 25, halign: 'left' } // Notes ]);
},
margin: { top: 85, left: 10, right: 10 }, const calculateLeftMargin = (
tableWidth: 'auto', columnStyles: Record<number, { cellWidth: number }>,
// Callback untuk styling baris pageWidth: number
didDrawCell: (data: any) => { ): number => {
// Tambahkan border pada setiap cell const totalTableWidth = Object.values(columnStyles).reduce(
if (data.section === 'body') { (sum, col) => sum + col.cellWidth,
pdf.setDrawColor(200, 200, 200); 0
pdf.setLineWidth(0.1); );
}
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,
startY: currentY,
styles: {
fontSize: 7,
cellPadding: 2,
overflow: 'linebreak',
halign: 'center'
},
headStyles: {
fillColor: [68, 114, 196],
textColor: [255, 255, 255],
fontStyle: 'bold',
halign: 'center'
},
columnStyles,
tableWidth: 'wrap',
margin: { top: currentY, left: leftMargin },
didDrawCell: (data: any) => {
if (data.section === 'body') {
pdf.setDrawColor(200, 200, 200);
pdf.setLineWidth(0.1);
}
}
});
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 (error) {
// Generate nama file
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`;
// Save file
pdf.save(fileName);
toast.success('PDF exported successfully');
} catch (error) {
console.error('Error exporting to PDF:', error); console.error('Error exporting to PDF:', error);
toast.error('Failed to export PDF'); toast.error('Failed to export PDF');
} }
}; };
const doExportData = async (sorting: any, filter: any) => { const doExportData = async (sorting: any, filter: any) => {
const user = localStorage.getItem( const user = localStorage.getItem(