diff --git a/package.json b/package.json index bc73844..be06bb0 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,13 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^3.0.0", + "file-saver": "^2.0.5", "formik": "^2.4.6", "helmet": "^8.1.0", "html2canvas": "^1.4.1", "https": "^1.0.0", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "leaflet": "^1.9.4", "lucide-react": "^0.456.0", "metronic-tailwind-react": "file:", @@ -86,10 +89,12 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "vite-plugin-windicss": "^1.9.3", + "xlsx": "^0.18.5", "yup": "^1.4.0" }, "devDependencies": { "@eslint/js": "^9.14.0", + "@types/file-saver": "^2.0.7", "@types/leaflet": "^1.9.14", "@types/node": "^22.9.0", "@types/react": "^18.3.12", diff --git a/public/media/avatars/KopDetailWalletStatement.png b/public/media/avatars/KopDetailWalletStatement.png new file mode 100644 index 0000000..a2c150b Binary files /dev/null and b/public/media/avatars/KopDetailWalletStatement.png differ diff --git a/src/auth/pages/jwt/Login.tsx b/src/auth/pages/jwt/Login.tsx index f15ae46..fec6898 100644 --- a/src/auth/pages/jwt/Login.tsx +++ b/src/auth/pages/jwt/Login.tsx @@ -17,12 +17,16 @@ const loginSchema = Yup.object().shape({ .min(3, 'Minimum 3 symbols') .max(50, 'Maximum 50 symbols') .required('Password is required'), + token: Yup.string(), + // .max(3, 'Maximum 6 code'), + // .required('Token is required'), remember: Yup.boolean() }); const initialValues = { username: '', password: '', + token: '', remember: false }; @@ -43,7 +47,7 @@ const Login = () => { try { if (!login) throw new Error('JWTProvider is required for this form.'); - await login(values.username, values.password); + await login(values.username, values.password, values.token); if (values.remember) { localStorage.setItem('username', values.username); @@ -141,6 +145,33 @@ const Login = () => { )} +
+ + + {formik.touched.token && formik.errors.token && ( + + {formik.errors.token} + + )} +
+
); }; -export default FilterSection; \ No newline at end of file +export default FilterSection; diff --git a/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx index 64e6521..2cd58be 100644 --- a/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx +++ b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx @@ -11,16 +11,29 @@ import { useCallApi } from '@/hooks'; import { apiConfig } from '@/config/api.config'; import { DataGridColumnHeader, DataGridProvider } from '@/components'; import { useManageStatementContext } from '../hooks/useManageWalletStatementContext'; -import { Toaster } from 'sonner'; +import { toast, Toaster } from 'sonner'; import { format } from 'date-fns'; import FilterSection from './FilterSection'; +import * as XLSX from 'xlsx'; +import jsPDF from 'jspdf'; +import 'jspdf-autotable'; + +interface AutoTableData { + pageNumber: number; + table: any; + doc: jsPDF; +} interface ContextProps { doExportData: (sorting: any, filter: any) => Promise; + doExportExcel: () => Promise; + doExportPDF: () => Promise; } const initialProps: ContextProps = { - doExportData: async () => ({ data: [], totalCount: 0 }) + doExportData: async () => ({ data: [], totalCount: 0 }), + doExportExcel: async () => {}, + doExportPDF: async () => {} }; export const showDetailWalletContext = createContext(initialProps); @@ -174,6 +187,336 @@ const ShowDetailWalletDialog = () => { [GetData, filters, selectedWallet] ); + const getAllTransactionData = async () => { + try { + const response = await GetData( + `${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${selectedWallet?.ID}`, + { + limit: 10000, + page: 1, + with_deleted: false, + order_field: 'amount', + order_direction: 'DESC', + filter: JSON.stringify(filters) + } + ); + + return response?.data?.list || []; + } catch (error) { + console.error('Error fetching all transaction data', error); + return []; + } + }; + + const formatTransactionData = (data: any[]) => { + return data.map((item) => ({ + 'Transaction Code': item.transaction_code || '-', + 'MSISDN Reffer': item.msisdn_reff || '-', + 'Transaction Type': item.transaction_type?.name || '-', + Type: item.type || '-', + 'Pre Amount': item.pre_amount?.toFixed(2) || '0.00', + Amount: item.amount?.toFixed(2) || '0.00', + 'Post Amount': item.post_amount?.toFixed(2) || '0.00', + Category: categoryMap[item.category] || '-', + Date: item.date + ? new Date(item.date).toLocaleString('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : '-', + Purpose: item.purpose || '-', + Notes: item.notes || '-' + })); + }; + + const doExportExcel = async () => { + try { + const allData = await getAllTransactionData(); + const formattedData = formatTransactionData(allData); + + const wb = XLSX.utils.book_new(); + + const ws = XLSX.utils.json_to_sheet([]); + + const companyHeader = [ + ['T•PAY'], + ['Telin Digital Solution'], + ['Av. Pres. Nicolau Lobato, Dili'], + ['Tel: (+670) 74147147 / 147'], + ['www.t-pay.tl'], + [''], + ['Wallet Statement Details'], + [`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`], + [`Wallet ID: ${selectedWallet?.ID || '-'}`], + [''] + ]; + + const columnHeaders = [ + [ + 'Transaction Code', + 'MSISDN Reffer', + 'Transaction Type', + 'Type', + 'Pre Amount', + 'Amount', + 'Post Amount', + 'Category', + 'Date', + 'Purpose', + 'Notes' + ] + ]; + + const excelData = [ + ...companyHeader, + ...columnHeaders, + ...formattedData.map((item) => Object.values(item)) + ]; + + XLSX.utils.sheet_add_aoa(ws, excelData); + + const colWidths = [ + { wch: 20 }, + { wch: 15 }, + { wch: 25 }, + { wch: 10 }, + { wch: 12 }, + { wch: 12 }, + { wch: 12 }, + { wch: 15 }, + { wch: 20 }, + { wch: 25 }, + { wch: 25 } + ]; + ws['!cols'] = colWidths; + + const tpayCell = XLSX.utils.encode_cell({ r: 0, c: 0 }); + if (ws[tpayCell]) { + ws[tpayCell].s = { + 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]) { + ws[titleCell].s = { + font: { bold: true, sz: 14 }, + alignment: { horizontal: 'center' } + }; + } + + for (let i = 7; i < 9; i++) { + const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 }); + if (ws[cellRef]) { + ws[cellRef].s = { + font: { sz: 11 }, + alignment: { horizontal: 'left' } + }; + } + } + + for (let i = 0; i < 6; i++) { + 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)); + } + + for (let col = 0; col < columnHeaders[0].length; col++) { + const cellRef = XLSX.utils.encode_cell({ r: 10, c: col }); + if (ws[cellRef]) { + ws[cellRef].s = { + font: { bold: true, color: { rgb: 'FFFFFF' } }, + fill: { fgColor: { rgb: '4472C4' } }, + alignment: { horizontal: 'center' }, + border: { + top: { style: 'thin', color: { rgb: '000000' } }, + bottom: { style: 'thin', color: { rgb: '000000' } }, + left: { style: 'thin', color: { rgb: '000000' } }, + right: { style: 'thin', color: { rgb: '000000' } } + } + }; + } + } + + for (let row = 11; row < 11 + 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 = { + border: { + top: { style: 'thin', color: { rgb: 'D9D9D9' } }, + bottom: { style: 'thin', color: { rgb: 'D9D9D9' } }, + left: { 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'); + + const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; + XLSX.writeFile(wb, fileName); + + // console.log('Excel export completed successfully'); + } catch (error) { + console.error('Error exporting to Excel:', error); + } + }; + + // Perbaikan untuk fungsi doExportPDF +const doExportPDF = async () => { + try { + // Import jsPDF dengan cara yang benar + const jsPDF = (await import('jspdf')).default; + + // Import autoTable plugin + const autoTable = (await import('jspdf-autotable')).default; + + const allData = await getAllTransactionData(); + const formattedData = formatTransactionData(allData); + + // Buat instance jsPDF + const pdf = new jsPDF({ + orientation: 'landscape', // Ubah ke landscape untuk tabel yang lebar + unit: 'mm', + format: 'a4' + }); + + // Header perusahaan + pdf.setFontSize(18); + pdf.setFont('helvetica', 'bold'); + pdf.text('T•PAY', pdf.internal.pageSize.getWidth() / 2, 20, { align: 'center' }); + + pdf.setFontSize(12); + 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.setLineWidth(0.5); + pdf.line(20, 52, pdf.internal.pageSize.getWidth() - 20, 52); + + // Judul laporan + pdf.setFontSize(16); + pdf.setFont('helvetica', 'bold'); + pdf.text('Wallet Statement Details', pdf.internal.pageSize.getWidth() / 2, 62, { align: 'center' }); + + // Informasi tambahan + pdf.setFontSize(10); + pdf.setFont('helvetica', 'normal'); + 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 + 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'] + ]); + + // Gunakan autoTable dengan cara yang benar + autoTable(pdf, { + head: [tableHeaders], + body: tableData, + startY: 85, + styles: { + fontSize: 7, + cellPadding: 2, + overflow: 'linebreak', + halign: 'center' + }, + headStyles: { + fillColor: [68, 114, 196], + textColor: [255, 255, 255], + fontStyle: 'bold', + halign: 'center' + }, + columnStyles: { + 0: { cellWidth: 25, halign: 'left' }, // Transaction Code + 1: { cellWidth: 20, halign: 'left' }, // MSISDN Reffer + 2: { cellWidth: 25, halign: 'left' }, // Transaction Type + 3: { cellWidth: 15, halign: 'center' }, // Type + 4: { cellWidth: 18, halign: 'right' }, // Pre Amount + 5: { cellWidth: 18, halign: 'right' }, // Amount + 6: { cellWidth: 18, halign: 'right' }, // Post Amount + 7: { cellWidth: 20, halign: 'left' }, // Category + 8: { cellWidth: 30, halign: 'left' }, // Date + 9: { cellWidth: 25, halign: 'left' }, // Purpose + 10: { cellWidth: 25, halign: 'left' } // Notes + }, + margin: { top: 85, left: 10, right: 10 }, + tableWidth: 'auto', + // Callback untuk styling baris + didDrawCell: (data: any) => { + // Tambahkan border pada setiap cell + if (data.section === 'body') { + pdf.setDrawColor(200, 200, 200); + pdf.setLineWidth(0.1); + } + } + }); + + // 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); + toast.error('Failed to export PDF'); + } +}; + const doExportData = async (sorting: any, filter: any) => { const user = localStorage.getItem( `${import.meta.env.VITE_APP_NAME}-auth-v${import.meta.env.VITE_APP_VERSION}` @@ -219,7 +562,7 @@ const ShowDetailWalletDialog = () => { }; return ( - +