UNPKG

exportdatafile

Version:
1 lines 76.1 kB
{"version":3,"sources":["../src/interface.ts","../src/helpers.ts","../src/exportExcel.ts","../src/exportPdf.ts","../src/exportTextFile.tsx","../src/index.ts"],"sourcesContent":["import jsPDF from \"jspdf\";\nimport ExcelJS from \"exceljs\";\n\ntype FormatType = \"RP\" | \"GR\" | \"DATETIME\" | \"NUMBER\" | \"IMAGE\" | \"\";\ntype HalignType = \"center\" | \"right\" | \"left\" | \"\";\ntype ValignType = \"top\" | \"middle\" | \"bottom\" | undefined;\n\nexport interface ColumnGenarator<T> {\n key: keyof T;\n label?: string;\n options?: {\n format?: FormatType;\n halign?: HalignType;\n valign?: ValignType;\n disabledColumn?: boolean;\n disabledFooter?: boolean;\n };\n child?: ColumnGenarator<T>[];\n}\n\nexport interface DataItemGenerator {\n [key: string]: any;\n}\n\nexport type FileType = \"EXCEL\" | \"PDF\" | \"TXT\" | \"ALL\";\n\nexport const validFileTypes: FileType[] = [\"EXCEL\", \"PDF\", \"TXT\", \"ALL\"];\ntype CustomizePdfFunction = (\n doc: jsPDF,\n finalY: number,\n autoTable?: any\n) => void;\ntype addRowPdfPdfFunction = (tableRows?: any) => void;\ntype CustomizeFunctionExcel = (worksheet: ExcelJS.Worksheet) => void;\n\nexport interface GenaratorExport<T> {\n columns: ColumnGenarator<T>[];\n data: DataItemGenerator[];\n type: (\"EXCEL\" | \"PDF\" | \"TXT\" | \"ALL\")[];\n title?: string;\n\n pdfSetting?: {\n orientation?: \"p\" | \"portrait\" | \"l\" | \"landscape\";\n unit?: \"pt\" | \"px\" | \"in\" | \"mm\" | \"cm\" | \"ex\" | \"em\" | \"pc\";\n width?: number;\n height?: number;\n fontSIze?: number;\n bgColor?: string;\n titlePdf?: string;\n txtColor?: string;\n finalY?: number;\n textHeaderRight?: string;\n textHeaderLeft?: string;\n theme?: \"grid\" | \"striped\" | \"plain\";\n grandTotalSetting?: {\n disableGrandTotal?: boolean;\n colSpan?: number;\n };\n openNewTab?: boolean;\n addRow?: addRowPdfPdfFunction;\n customize?: CustomizePdfFunction;\n disablePrintDate?: boolean;\n };\n date?: {\n start_date?: string;\n end_date?: string;\n caption?: string;\n };\n txtSetting?: {\n dataTxt?: DataItemGenerator[] | DataItemGenerator;\n titleTxt: string;\n templateTxt?: string;\n copy?: boolean;\n };\n excelSetting?: {\n titleExcel?: string;\n bgColor?: string;\n txtColor?: string;\n additionalTextHeader?: string;\n grandTotalSetting?: {\n disableGrandTotal?: boolean;\n colSpan?: number;\n };\n customize?: CustomizeFunctionExcel;\n };\n grouping: string[];\n footerSetting?: {\n subTotal?: {\n caption?: string;\n enableCount?: boolean;\n captionItem?: string;\n disableSubtotal?: boolean;\n };\n grandTotal?: {\n caption?: string;\n captionItem?: string;\n enableCount?: boolean;\n disableGrandTotal?: boolean;\n };\n };\n}\n","import ExcelJS from 'exceljs';\nimport { ColumnGenarator, FileType, validFileTypes } from \"./interface\";\n\nexport function convertDateTime(tgl: string) {\n const now = new Date(tgl);\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, \"0\");\n const day = String(now.getDate()).padStart(2, \"0\");\n const hours = String(now.getHours()).padStart(2, \"0\");\n const minutes = String(now.getMinutes()).padStart(2, \"0\");\n const seconds = String(now.getSeconds()).padStart(2, \"0\");\n const currentDateTime = `${day}-${month}-${year} ${hours}:${minutes}:${seconds}`;\n return currentDateTime;\n}\n\nexport function validateFileTypes(fileTypes: FileType[]): boolean {\n return fileTypes.every((fileType) => validFileTypes.includes(fileType));\n}\n\nexport function countColumns(columns: ColumnGenarator<any>[]): number {\n let count = 0;\n columns.forEach((col) => {\n if (col.child && col.child.length > 0) {\n count += countColumns(col.child);\n } else {\n count += 1;\n }\n });\n return count;\n}\n\nexport const getFlattenColumns = (\n columns: ColumnGenarator<any>[]\n): ColumnGenarator<any>[] => {\n const flat: ColumnGenarator<any>[] = [];\n\n columns.forEach((col) => {\n if (col.child && col.child.length > 0) {\n flat.push(...getFlattenColumns(col.child)); // panggil rekursif\n } else {\n flat.push(col);\n }\n });\n\n return flat;\n};\n\nexport const formatingTitle = (title: string): string => {\n // Pisahkan kata-kata menggunakan underscore sebagai pemisah\n const words = title.split(\"_\");\n\n // Ubah setiap kata menjadi huruf kapital dan gabungkan kembali dengan spasi di antara mereka\n const formattedtitle = words\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n\n return formattedtitle;\n};\n\n\n/**\n * Menambahkan gambar ke baris Excel menggunakan ExcelJS di browser, dari base64 string\n */\nexport async function addImagesToRow(\n workbook: ExcelJS.Workbook,\n worksheet: ExcelJS.Worksheet,\n row: ExcelJS.Row,\n rowData: any[]\n) {\n for (let index = 0; index < rowData.length; index++) {\n const cellData = rowData[index];\n\n // Proses jika imageSrc adalah base64 string\n if (\n cellData?.isImage &&\n cellData?.imageSrc &&\n typeof cellData.imageSrc === \"string\" &&\n cellData.imageSrc.startsWith(\"data:image\")\n ) {\n try {\n const { buffer, extension } = base64ToBufferAndExtension(cellData.imageSrc);\n\n const imageId = workbook.addImage({\n buffer: buffer as any,\n extension,\n });\n\n worksheet.addImage(imageId, {\n tl: { col: index, row: row.number - 1 },\n ext: { width: 70, height: 60 },\n editAs: \"oneCell\",\n });\n\n if (!row.height || row.height < 60) {\n row.height = 35;\n }\n } catch (error) {\n console.error(\"❌ Error adding image to row:\", error);\n row.getCell(index + 1).value = \"[Image]\";\n }\n }\n }\n}\n\n/**\n * Mengubah base64 image string ke buffer dan extension\n */\nfunction base64ToBufferAndExtension(base64: string): { buffer: Uint8Array; extension: any } {\n const match = base64.match(/^data:image\\/(\\w+);base64,(.*)$/);\n if (!match) throw new Error(\"Invalid base64 image format\");\n const extension = match[1] === \"jpg\" ? \"jpeg\" : (match[1] as any);\n const base64Data = match[2];\n const binaryString = atob(base64Data);\n const len = binaryString.length;\n const bytes = new Uint8Array(len);\n for (let i = 0; i < len; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return { buffer: bytes, extension };\n}","// import BwipJs from \"bwip-js/browser\";\nimport {\n addImagesToRow,\n convertDateTime,\n countColumns,\n formatingTitle,\n getFlattenColumns\n} from \"./helpers\";\nimport { DataItemGenerator, GenaratorExport } from \"./interface\";\nimport ExcelJS from \"exceljs\";\nconst ExportExcel = async <T>({\n columns,\n data,\n grouping,\n date,\n excelSetting,\n title,\n footerSetting,\n}: GenaratorExport<T>): Promise<void> => {\n const workbook = new ExcelJS.Workbook();\n columns = columns.filter((item) => !item.options?.disabledColumn);\n const worksheet = workbook.addWorksheet(title || excelSetting?.titleExcel);\n\n const lastUsedColumnIndex = countColumns(columns);\n\n // Judul\n // console.log(lastUsedColumnIndex);\n const judul = worksheet.addRow([]);\n judul.getCell(1).value = title || excelSetting?.titleExcel;\n judul.getCell(1).alignment = { horizontal: \"center\" };\n\n const lastColumnLetter = worksheet.getColumn(lastUsedColumnIndex).letter;\n\n worksheet.mergeCells(`A${judul.number}:${lastColumnLetter}${judul.number}`);\n\n judul.eachCell((cell) => {\n cell.font = {\n color: { argb: \"000000\" },\n bold: true,\n size: 12,\n };\n });\n\n // Tanggal\n if (date) {\n const tanggalRow = worksheet.addRow([]);\n tanggalRow.getCell(1).value =\n `${date.caption ? date.caption : \"Tanggal \"} : ${date?.start_date} ${date?.end_date ? `s/d ${date?.end_date}` : \"\"\n }`;\n tanggalRow.getCell(1).alignment = { horizontal: \"center\" };\n worksheet.mergeCells(\n `A${tanggalRow.number}:${lastColumnLetter}${tanggalRow.number}`\n );\n\n tanggalRow.eachCell((cell) => {\n cell.font = {\n color: { argb: \"00000\" },\n bold: true,\n size: 12,\n };\n });\n }\n\n // additional\n const additionalText = worksheet.addRow([]);\n additionalText.getCell(1).value = excelSetting?.additionalTextHeader || \"\";\n additionalText.getCell(1).alignment = { horizontal: \"center\" };\n worksheet.mergeCells(\n `A${additionalText.number}:${lastColumnLetter}${additionalText.number}`\n );\n\n additionalText.eachCell((cell) => {\n cell.font = { color: { argb: \"000000\" }, bold: true, size: 12 };\n });\n\n const hasChild = columns.some((col) => col.child && col.child.length > 0);\n const headerColumn1 = worksheet.addRow([]);\n const headerColumn2 = hasChild ? worksheet.addRow([]) : null;\n\n columns.forEach((col) => {\n if (col.child && col.child.length > 0) {\n // Parent dengan child: di headerColumn1 ditulis parent-nya dan di headerColumn2 tulis child-nya\n // Merge parent cell di headerColumn1 sesuai jumlah child\n const startCol = headerColumn1.actualCellCount + 1;\n const childCount = col.child.length;\n\n // Isi parent di headerColumn1, merge sesuai childCount\n headerColumn1.getCell(startCol).value = col.label;\n if (childCount > 1) {\n worksheet.mergeCells(\n headerColumn1.number,\n startCol,\n headerColumn1.number,\n startCol + childCount - 1\n );\n }\n\n // Styling dan alignment parent headerColumn1\n for (let i = startCol; i < startCol + childCount; i++) {\n const cell = headerColumn1.getCell(i);\n cell.fill = {\n type: \"pattern\",\n pattern: \"solid\",\n fgColor: { argb: excelSetting?.bgColor || \"E8E5E5\" },\n };\n cell.font = {\n color: { argb: excelSetting?.txtColor || \"000000\" },\n bold: true,\n };\n cell.alignment = { horizontal: \"center\", vertical: \"middle\" };\n }\n\n // Isi child di headerColumn2\n col.child.forEach((childCol, index) => {\n if (headerColumn2) {\n const cell = headerColumn2.getCell(startCol + index);\n cell.value = childCol.label;\n cell.fill = {\n type: \"pattern\",\n pattern: \"solid\",\n fgColor: { argb: excelSetting?.bgColor || \"E8E5E5\" },\n };\n cell.font = {\n color: { argb: excelSetting?.txtColor || \"000000\" },\n bold: true,\n };\n\n const halign =\n childCol.options?.halign ||\n ([\"RP\", \"GR\", \"NUMBER\"].includes(childCol?.options?.format || \"\")\n ? \"right\"\n : \"left\");\n const valign = childCol.options?.valign || \"middle\";\n cell.alignment = { horizontal: halign, vertical: valign };\n }\n });\n } else {\n // Parent tanpa child: tulis di headerColumn1 dan merge di headerColumn2 agar melebar ke bawah\n const colIndex = headerColumn1.actualCellCount + 1;\n\n // Isi headerColumn1\n headerColumn1.getCell(colIndex).value = col.label;\n headerColumn1.getCell(colIndex).fill = {\n type: \"pattern\",\n pattern: \"solid\",\n fgColor: { argb: excelSetting?.bgColor || \"E8E5E5\" },\n };\n headerColumn1.getCell(colIndex).font = {\n color: { argb: excelSetting?.txtColor || \"000000\" },\n bold: true,\n };\n headerColumn1.getCell(colIndex).alignment = {\n horizontal: col.options?.halign || \"center\",\n vertical: col.options?.valign || \"middle\",\n };\n if (hasChild && headerColumn2) {\n worksheet.mergeCells(\n headerColumn1.number,\n colIndex,\n headerColumn2.number,\n colIndex\n );\n }\n }\n });\n\n const totals: { [key: string]: number } = {};\n\n data.forEach(async (item) => {\n if (grouping.length > 0) {\n const totalColumns = countColumns(columns); // sudah kamu punya\n const groupContent = grouping\n .map((column) =>\n item[column] !== undefined\n ? `${formatingTitle(column)} : ${item[column]}`\n : \"\"\n )\n .filter(Boolean)\n .join(\" | \");\n\n const groupRow = worksheet.addRow([groupContent]); // hanya isi satu sel (di kolom A)\n worksheet.mergeCells(\n `A${groupRow.number}:${String.fromCharCode(64 + totalColumns)}${groupRow.number}`\n );\n\n // Styling opsional:\n groupRow.getCell(1).alignment = { horizontal: \"left\" };\n groupRow.getCell(1).font = { bold: true };\n\n const subtotal: { [key: string]: number } = {};\n\n item.detail.forEach(async (itemDetail: any) => {\n const flatColumns = getFlattenColumns(columns);\n\n const rowData = flatColumns.map((column) => {\n if (\n column?.options?.format === \"IMAGE\" &&\n itemDetail[column.key as keyof DataItemGenerator]\n ) {\n return {\n value: \"\", // Kosongkan value untuk cell gambar\n alignment: { horizontal: \"center\", vertical: \"middle\" },\n isImage: true,\n imageSrc: itemDetail[column.key as keyof DataItemGenerator], // base64 sudah didukung oleh fetchImageAsBuffer\n };\n }\n const value =\n column?.options?.format === \"DATETIME\"\n ? convertDateTime(\n itemDetail[column.key as keyof DataItemGenerator]\n )\n : itemDetail[column.key as keyof DataItemGenerator];\n const alignment = {\n horizontal: column?.options?.halign\n ? column?.options?.halign\n : column?.options?.format === \"RP\" ||\n column?.options?.format === \"GR\" ||\n column?.options?.format === \"NUMBER\"\n ? \"right\"\n : \"left\",\n };\n const columnKey = column.key as keyof DataItemGenerator;\n totals[columnKey] = (totals[columnKey] || 0) + Number(value);\n subtotal[columnKey] = (subtotal[columnKey] || 0) + Number(value);\n\n return {\n value,\n alignment,\n numFmt:\n column?.options?.format === \"RP\"\n ? \"#,##0\"\n : column?.options?.format === \"GR\"\n ? \"#,##0.000\"\n : undefined,\n };\n });\n\n const row = worksheet.addRow(rowData.map((cellData) => cellData.value));\n\n rowData.forEach((cellData, index) => {\n const cell = row.getCell(index + 1);\n // Only assign allowed values for horizontal and vertical alignment\n const allowedHorizontal: Array<\"center\" | \"right\" | \"left\" | \"fill\" | \"justify\" | \"centerContinuous\" | \"distributed\"> = [\n \"center\", \"right\", \"left\", \"fill\", \"justify\", \"centerContinuous\", \"distributed\"\n ];\n const allowedVertical: Array<\"top\" | \"middle\" | \"bottom\" | \"distributed\" | \"justify\"> = [\n \"top\", \"middle\", \"bottom\", \"distributed\", \"justify\"\n ];\n\n let horizontal: typeof allowedHorizontal[number] | undefined = undefined;\n let vertical: typeof allowedVertical[number] | undefined = undefined;\n\n if (cellData.alignment && typeof cellData.alignment.horizontal === \"string\" && allowedHorizontal.includes(cellData.alignment.horizontal as any)) {\n horizontal = cellData.alignment.horizontal as typeof allowedHorizontal[number];\n }\n if (\n cellData.alignment &&\n \"vertical\" in cellData.alignment &&\n typeof (cellData.alignment as any).vertical === \"string\" &&\n allowedVertical.includes((cellData.alignment as any).vertical)\n ) {\n vertical = (cellData.alignment as any).vertical as typeof allowedVertical[number];\n }\n\n cell.alignment = {\n ...(horizontal ? { horizontal } : {}),\n ...(vertical ? { vertical } : {}),\n };\n\n // Set numFmt only for number cells\n if (\n cellData.numFmt &&\n typeof cellData.value === \"number\" &&\n !isNaN(cellData.value)\n ) {\n // Ensure the cell value is a number before setting numFmt\n cell.value = Number(cellData.value);\n cell.numFmt = cellData.numFmt;\n }\n });\n\n // Add images after all cell formatting is complete\n await addImagesToRow(workbook, worksheet, row, rowData);\n });\n const flatColumnsSubTott = getFlattenColumns(columns);\n\n if (!footerSetting?.subTotal?.disableSubtotal) {\n const subtotalRow = worksheet.addRow(columns.map(() => null)); // Create a row with null values\n\n flatColumnsSubTott.forEach((column, columnIndex) => {\n if (\n column?.options?.format === \"RP\" ||\n column?.options?.format === \"GR\" ||\n column?.options?.format === \"NUMBER\"\n ) {\n const startRow = 4; // Adjust this based on the starting row for your data\n const endRow = data.length + startRow - 1;\n const totalFormula = `SUM(${String.fromCharCode(\n 65 + columnIndex\n )}${startRow}:${String.fromCharCode(65 + columnIndex)}${endRow})`;\n const grandTotalCell = subtotalRow.getCell(columnIndex + 1);\n const subtotalCount = footerSetting?.subTotal?.enableCount\n ? grouping.length > 0\n ? \" : \" + item.detail.length\n : \"\"\n : \"\";\n\n const captionSub = footerSetting?.subTotal?.captionItem\n ? footerSetting?.subTotal?.captionItem\n : \"\";\n (subtotalRow.getCell(1).value =\n `${footerSetting?.subTotal?.caption || \"SUB TOTAL\"} ${subtotalCount} ${captionSub}`),\n (subtotalRow.getCell(1).alignment = { horizontal: \"center\" });\n\n // Explicitly cast the cell to CellValue to set numFmt\n (grandTotalCell as any).numFmt =\n column?.options?.format === \"GR\" ? \"#,##0.000\" : \"#,##0\";\n grandTotalCell.value = { formula: totalFormula };\n subtotalRow.getCell(columnIndex + 1).value = column?.options\n .disabledFooter\n ? \"\"\n : subtotal[column.key as keyof DataItemGenerator];\n } else {\n subtotalRow.getCell(columnIndex + 1).value = \"\";\n }\n });\n\n\n if (excelSetting?.grandTotalSetting?.colSpan) {\n worksheet.mergeCells(\n `A${subtotalRow.number}:${String.fromCharCode(\n 64 + Number(excelSetting?.grandTotalSetting?.colSpan)\n )}${subtotalRow.number}`\n );\n }\n subtotalRow.eachCell((cell) => {\n cell.fill = {\n type: \"pattern\",\n pattern: \"solid\",\n fgColor: { argb: excelSetting?.bgColor || \"#E8E5E5\" }, // Warna hijau yang diinginkan\n bgColor: { argb: excelSetting?.bgColor || \"#E8E5E5\" },\n };\n cell.font = {\n color: { argb: excelSetting?.txtColor },\n bold: true,\n };\n });\n }\n\n } else {\n const flatColumns = getFlattenColumns(columns);\n\n const rowData = flatColumns.map((column) => {\n if (\n column?.options?.format === \"IMAGE\" &&\n item[column.key as keyof DataItemGenerator]\n ) {\n return {\n value: \"\", // Kosongkan value untuk cell gambar\n alignment: { horizontal: \"center\", vertical: \"middle\" },\n isImage: true,\n imageSrc: item[column.key as keyof DataItemGenerator], // base64 sudah didukung oleh fetchImageAsBuffer\n };\n }\n const value =\n column?.options?.format === \"DATETIME\"\n ? convertDateTime(\n item[column.key as keyof DataItemGenerator]\n )\n : item[column.key as keyof DataItemGenerator];\n const alignment = {\n horizontal: column?.options?.halign\n ? column?.options?.halign\n : column?.options?.format === \"RP\" ||\n column?.options?.format === \"GR\" ||\n column?.options?.format === \"NUMBER\"\n ? \"right\"\n : \"left\",\n };\n const columnKey = column.key as keyof DataItemGenerator;\n totals[columnKey] = (totals[columnKey] || 0) + Number(value);\n\n return {\n value,\n alignment,\n numFmt:\n column?.options?.format === \"RP\"\n ? \"#,##0\"\n : column?.options?.format === \"GR\"\n ? \"#,##0.000\"\n : undefined,\n };\n });\n\n // BUG: rowData.map(async (cellData) => cellData.value) menghasilkan array Promise, bukan array value!\n // Seharusnya: rowData.map(cellData => cellData.value)\n const row = worksheet.addRow(rowData.map(cellData => cellData.value));\n\n rowData.forEach((cellData, index) => {\n const cell = row.getCell(index + 1);\n // Only assign allowed values for horizontal and vertical alignment\n const allowedHorizontal: Array<\"center\" | \"right\" | \"left\" | \"fill\" | \"justify\" | \"centerContinuous\" | \"distributed\"> = [\n \"center\", \"right\", \"left\", \"fill\", \"justify\", \"centerContinuous\", \"distributed\"\n ];\n const allowedVertical: Array<\"top\" | \"middle\" | \"bottom\" | \"distributed\" | \"justify\"> = [\n \"top\", \"middle\", \"bottom\", \"distributed\", \"justify\"\n ];\n\n let horizontal: typeof allowedHorizontal[number] | undefined = undefined;\n let vertical: typeof allowedVertical[number] | undefined = undefined;\n\n if (cellData.alignment && typeof cellData.alignment.horizontal === \"string\" && allowedHorizontal.includes(cellData.alignment.horizontal as any)) {\n horizontal = cellData.alignment.horizontal as typeof allowedHorizontal[number];\n }\n if (\n cellData.alignment &&\n \"vertical\" in cellData.alignment &&\n typeof (cellData.alignment as any).vertical === \"string\" &&\n allowedVertical.includes((cellData.alignment as any).vertical)\n ) {\n vertical = (cellData.alignment as any).vertical as typeof allowedVertical[number];\n }\n\n cell.alignment = {\n ...(horizontal ? { horizontal } : {}),\n ...(vertical ? { vertical } : {}),\n };\n\n if (\n cellData.numFmt &&\n typeof cellData.value === \"number\" &&\n !isNaN(cellData.value)\n ) {\n // Ensure the cell value is a number before setting numFmt\n cell.value = Number(cellData.value);\n cell.numFmt = cellData.numFmt;\n }\n });\n\n // Add images after all cell formatting is complete\n await addImagesToRow(workbook, worksheet, row, rowData);\n\n }\n });\n\n const grandTotalRow = worksheet.addRow(columns.map(() => null)); // Create a row with null values\n\n const flatColumnsTott = getFlattenColumns(columns);\n\n if (!footerSetting?.grandTotal?.disableGrandTotal) {\n\n flatColumnsTott.forEach((column, columnIndex) => {\n if (\n column?.options?.format === \"RP\" ||\n column?.options?.format === \"GR\" ||\n column?.options?.format === \"NUMBER\"\n ) {\n\n const startRow = 4; // Adjust this based on the starting row for your data\n const endRow = data.length + startRow - 1;\n const totalFormula = `SUM(${String.fromCharCode(\n 65 + columnIndex\n )}${startRow}:${String.fromCharCode(65 + columnIndex)}${endRow})`;\n const grandTotalCell = grandTotalRow.getCell(columnIndex + 1);\n // content: ,\n const GrandTotal = footerSetting?.grandTotal?.enableCount\n ? grouping.length > 0\n ? \" : \" +\n data.map((list) => list.detail.length).reduce((a, b) => a + b, 0)\n : \" : \" + data.length\n : \"\";\n\n const caption = footerSetting?.grandTotal?.captionItem\n ? footerSetting?.grandTotal?.captionItem\n : \"\";\n\n const footerGrandtotal = `${footerSetting?.grandTotal?.caption || \"GRAND TOTAL\"} ${GrandTotal} ${caption}`;\n grandTotalRow.getCell(1).value = footerGrandtotal;\n grandTotalRow.getCell(1).alignment = { horizontal: \"center\" };\n\n\n (grandTotalCell as any).numFmt =\n column?.options?.format === \"GR\" ? \"#,##0.000\" : \"#,##0\";\n grandTotalCell.value = { formula: totalFormula };\n grandTotalRow.getCell(columnIndex + 1).value = column?.options\n .disabledFooter\n ? \"\"\n : totals[column.key as keyof DataItemGenerator];\n } else {\n grandTotalRow.getCell(columnIndex + 1).value = \"\";\n }\n });\n\n if (excelSetting?.grandTotalSetting?.colSpan) {\n worksheet.mergeCells(\n `A${grandTotalRow.number}:${String.fromCharCode(\n 64 + Number(excelSetting?.grandTotalSetting?.colSpan)\n )}${grandTotalRow.number}`\n );\n }\n grandTotalRow.eachCell((cell) => {\n cell.fill = {\n type: \"pattern\",\n pattern: \"solid\",\n fgColor: { argb: excelSetting?.bgColor || \"#E8E5E5\" }, // Warna hijau yang diinginkan\n bgColor: { argb: excelSetting?.bgColor || \"#E8E5E5\" },\n };\n cell.font = {\n color: { argb: excelSetting?.txtColor },\n bold: true,\n };\n });\n }\n\n\n if (excelSetting?.customize) {\n excelSetting.customize(worksheet);\n }\n\n const buffer = await workbook.xlsx.writeBuffer();\n const blob = new Blob([buffer], {\n type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n });\n const link = document.createElement(\"a\");\n link.href = URL.createObjectURL(blob);\n link.download = `${excelSetting?.titleExcel || title}.xlsx`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n};\n\nexport default ExportExcel;\n","import {\n ColumnGenarator,\n DataItemGenerator,\n GenaratorExport\n} from \"./interface\";\nimport {\n convertDateTime,\n countColumns,\n formatingTitle,\n getFlattenColumns\n} from \"./helpers\";\nimport jsPDF from \"jspdf\";\nimport autoTable from \"jspdf-autotable\";\n\nconst ExportPDF = <T>({\n columns,\n data,\n grouping,\n pdfSetting,\n date,\n title,\n footerSetting,\n}: GenaratorExport<T>): void => {\n const doc: jsPDF = new jsPDF(pdfSetting?.orientation, pdfSetting?.unit, [\n pdfSetting?.width || 297,\n pdfSetting?.height || 210,\n ]);\n let tableRows: any[] = [];\n let finalY = date ? 30 : 20;\n columns = columns.filter((item) => !item.options?.disabledColumn);\n\n doc.setFontSize(10);\n const widthPortrait = doc.internal.pageSize.getWidth();\n\n const headerLeft = doc.splitTextToSize(pdfSetting?.textHeaderLeft || \"\", 110);\n doc.text(headerLeft, 15, 18);\n\n //Text Kanan\n doc.text(`${title || pdfSetting?.titlePdf}`, widthPortrait - 15, 18, {\n align: \"right\",\n });\n\n if (date) {\n doc.text(\n `${date.caption ? date.caption : \"TANGGAL \"} : ${date?.start_date} ${date?.end_date ? `s/d ${date?.end_date}` : \"\"\n }`,\n widthPortrait - 15,\n 22,\n { align: \"right\" }\n );\n }\n doc.setProperties({\n title: title || pdfSetting?.titlePdf,\n });\n\n if (pdfSetting?.finalY) {\n console.log(pdfSetting.finalY);\n finalY = pdfSetting.finalY;\n }\n\n // Header Tabel\n const headerRow1: any[] = [];\n const headerRow2: any[] = [];\n const hasChild = columns.some((col) => col.child && col.child.length > 0);\n columns.forEach((column) => {\n const baseStyle = {\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"bold\",\n ...(column?.options?.valign ? { valign: column.options.valign } : {}),\n halign:\n column?.options?.halign ??\n ([\"RP\", \"GR\", \"NUMBER\"].includes(column?.options?.format || \"\")\n ? \"right\"\n : \"left\"),\n };\n\n if (hasChild) {\n if (column.child && column.child.length > 0) {\n // Parent di headerRow1\n headerRow1.push({\n content: column.label,\n colSpan: column.child.length,\n styles: baseStyle,\n });\n\n // Children di headerRow2\n column.child.forEach((childCol) => {\n headerRow2.push({\n content: childCol.label,\n key: childCol.key,\n options: childCol.options,\n styles: {\n ...baseStyle,\n halign:\n childCol?.options?.halign ??\n ([\"RP\", \"GR\", \"NUMBER\"].includes(\n childCol?.options?.format || \"\"\n )\n ? \"right\"\n : \"left\"),\n },\n });\n });\n } else {\n // Tidak ada child, tapi harus isi 2 baris header (rowSpan 2)\n headerRow1.push({\n content: column.label,\n rowSpan: 2,\n key: column.key,\n options: column.options,\n styles: baseStyle,\n });\n }\n } else {\n // Semua column tidak punya child, cukup 1 headerRow\n headerRow1.push({\n content: column.label,\n key: column.key,\n options: column.options,\n styles: baseStyle,\n });\n }\n });\n\n // Push header ke tabel\n tableRows.push(headerRow1);\n if (hasChild && headerRow2.length > 0) {\n tableRows.push(headerRow2);\n }\n\n // Body Tabel\n const totals: { [key: string]: number } = {};\n\n data.forEach((item) => {\n if (grouping.length > 0) {\n // Tambah row grup (header kelompok)\n const totalColumns = countColumns(columns);\n\n const groupContent = grouping\n .map((column) =>\n item[column] !== undefined\n ? `${formatingTitle(column)} : ${item[column]}`\n : \"\"\n )\n .filter(Boolean) // hilangkan string kosong\n .join(\" | \"); // separator antar grup (bisa diganti sesuai preferensi)\n\n // console.log(groupContent)\n const groupRow = [\n {\n content: groupContent,\n colSpan: totalColumns, // colSpan sesuai jumlah kolom tabel\n styles: {\n fontStyle: \"bold\",\n halign: \"left\", // bisa disesuaikan\n },\n },\n ];\n\n tableRows.push(groupRow);\n\n const subtotal: { [key: string]: number } = {};\n\n // FLATTEN COLUMNS untuk looping\n const flatColumns = getFlattenColumns(columns);\n\n item.detail.forEach((list2: any) => {\n const rowData = flatColumns.map((column) => {\n const value = list2[column.key as keyof DataItemGenerator];\n const columnKey = column.key as keyof DataItemGenerator;\n\n // Hitung subtotal & total\n totals[columnKey] = (totals[columnKey] || 0) + Number(value || 0);\n subtotal[columnKey] = (subtotal[columnKey] || 0) + Number(value || 0);\n const isImage = column.options?.format === \"IMAGE\";\n\n return {\n content: (() => {\n switch (column?.options?.format) {\n case \"RP\":\n return value !== undefined\n ? Number(value || 0).toLocaleString(\"kr-ko\")\n : \"\";\n case \"GR\":\n return value !== undefined\n ? Number(value || 0).toFixed(3)\n : \"\";\n case \"NUMBER\":\n return value !== undefined ? Number(value || 0) : \"\";\n case \"IMAGE\":\n return \"\"\n case \"DATETIME\":\n return value !== undefined\n ? convertDateTime(value || new Date())\n : \"\";\n default:\n return value !== undefined ? value.toString() : \"\";\n }\n })(),\n foto: isImage ? value : null,\n styles: {\n halign: column?.options?.halign\n ? column?.options?.halign\n : column?.options?.format === \"RP\" ||\n column?.options?.format === \"GR\" ||\n column?.options?.format === \"NUMBER\"\n ? \"right\"\n : typeof value === \"number\"\n ? \"right\"\n : \"left\",\n },\n };\n });\n\n\n tableRows.push(rowData);\n });\n\n\n if (!footerSetting?.subTotal?.disableSubtotal) {\n\n // Footer Subtotal\n const footersubtotal: any = [];\n flatColumns.forEach((column) => {\n const total = subtotal[column.key as keyof DataItemGenerator];\n if (\n column?.options?.format === \"RP\" ||\n column?.options?.format === \"GR\" ||\n column?.options?.format === \"NUMBER\"\n ) {\n footersubtotal.push({\n content: column?.options?.disabledFooter\n ? \"\"\n : (() => {\n switch (column?.options?.format) {\n case \"RP\":\n return total.toLocaleString(\"kr-ko\");\n case \"GR\":\n return total.toFixed(3);\n case \"NUMBER\":\n return total;\n default:\n return total.toString();\n }\n })(),\n styles: {\n halign: column?.options?.halign || \"right\",\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"bold\",\n },\n });\n } else {\n footersubtotal.push({\n content: \"\",\n styles: {\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"bold\",\n },\n });\n }\n\n\n });\n\n const colSpan = pdfSetting?.grandTotalSetting?.colSpan\n ? Number(pdfSetting?.grandTotalSetting?.colSpan || 0) + 1\n : 0;\n\n const subtotalCount = footerSetting?.subTotal?.enableCount\n ? grouping.length > 0\n ? \" : \" + item.detail.length\n : \"\"\n : \"\";\n\n const captionSub = footerSetting?.subTotal?.captionItem\n ? footerSetting?.subTotal?.captionItem\n : \"\";\n\n footersubtotal[0] = {\n content: `${footerSetting?.subTotal?.caption || \"SUB TOTAL\"}${subtotalCount} ${captionSub}`,\n colSpan,\n styles: {\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"bold\",\n halign: \"center\",\n },\n };\n\n if (pdfSetting?.grandTotalSetting?.colSpan) {\n footersubtotal.splice(1, pdfSetting?.grandTotalSetting?.colSpan);\n }\n\n tableRows.push(footersubtotal);\n }\n } else {\n // Helper untuk mengambil data cell dari item dan column\n const getCellData = (column: ColumnGenarator<any>, item: any) => {\n const value = item[column.key as keyof DataItemGenerator];\n const columnKey = column.key as keyof DataItemGenerator;\n\n // Hitung total jika tidak disabled\n if (!column.options?.disabledFooter) {\n totals[columnKey] = (totals[columnKey] || 0) + Number(value || 0);\n }\n\n // Tentukan isi cell\n const content = (() => {\n switch (column?.options?.format) {\n case \"RP\":\n return value !== undefined\n ? Number(value || 0).toLocaleString(\"kr-ko\")\n : \"\";\n case \"GR\":\n return value !== undefined ? Number(value || 0).toFixed(3) : \"\";\n case \"NUMBER\":\n return value !== undefined ? Number(value || 0) : \"\";\n case \"IMAGE\":\n return '';\n case \"DATETIME\":\n return value !== undefined\n ? convertDateTime(value || new Date())\n : \"\";\n default:\n return value !== undefined ? value?.toString() : \"\";\n }\n })();\n\n // Style cell\n const halign = column?.options?.halign\n ? column?.options?.halign\n : column?.options?.format === \"RP\" ||\n column?.options?.format === \"GR\" ||\n column?.options?.format === \"NUMBER\"\n ? \"right\"\n : typeof value === \"number\"\n ? \"right\"\n : \"left\";\n\n const isImage = column.options?.format === \"IMAGE\";\n\n\n return {\n options: column?.options,\n content,\n foto: isImage ? value : null,\n styles: { halign },\n };\n };\n\n // Helper rekursif untuk flatten column dengan child\n\n // Dapatkan semua kolom flatten dari struktur kolom dengan child\n const flatColumns = getFlattenColumns(columns);\n\n // Lalu generate rowData\n const rowData = flatColumns.map((column) => getCellData(column, item));\n\n // Tambahkan ke baris tabel\n tableRows.push(rowData);\n }\n });\n\n const flatColumns = getFlattenColumns(columns);\n if (!footerSetting?.grandTotal?.disableGrandTotal) {\n const grandTotal: any[] = [];\n\n flatColumns.forEach((column) => {\n const total = totals[column.key as keyof DataItemGenerator];\n\n const isNumericFormat = [\"RP\", \"GR\", \"NUMBER\"].includes(\n column?.options?.format || \"\"\n );\n\n const content = column?.options?.disabledFooter\n ? \"\"\n : (() => {\n if (!isNumericFormat) return \"\";\n switch (column.options?.format) {\n case \"RP\":\n return Number(total || 0).toLocaleString(\"kr-KO\");\n case \"GR\":\n return Number(total || 0).toFixed(3);\n case \"NUMBER\":\n return Number(total || 0);\n default:\n return (total || 0).toString();\n }\n })();\n\n grandTotal.push({\n options: column?.options,\n content,\n styles: {\n halign: column?.options?.halign\n ? column.options.halign\n : isNumericFormat\n ? \"right\"\n : \"left\",\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"bold\",\n },\n });\n });\n // Tangani caption dan colSpan untuk GRAND TOTAL\n const rawColSpan = Number(pdfSetting?.grandTotalSetting?.colSpan || 0);\n const colSpan = Math.min(rawColSpan + 1, flatColumns.length); // batasi agar tidak lebih dari kolom yang tersedia\n\n const totalItemCount = footerSetting?.grandTotal?.enableCount\n ? grouping.length > 0\n ? data.reduce((sum, group) => sum + group.detail.length, 0)\n : data.length\n : 0;\n\n const caption = footerSetting?.grandTotal?.captionItem || \"\";\n const grandTotalLabel =\n `${footerSetting?.grandTotal?.caption || \"GRAND TOTAL\"}` +\n (totalItemCount ? ` : ${totalItemCount}` : \"\") +\n (caption ? ` ${caption}` : \"\");\n\n // Ubah cell pertama dengan content Grand Total + colSpan\n grandTotal[0] = {\n content: grandTotalLabel,\n colSpan,\n styles: {\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"bold\",\n halign: \"center\",\n },\n };\n\n // Hapus kolom setelah grandTotal[0] sebanyak colSpan - 1 agar panjang array tetap flatColumns.length\n grandTotal.splice(1, colSpan - 1);\n\n // Pastikan panjang array tetap sama dengan flatColumns.length\n while (grandTotal.length < flatColumns.length) {\n grandTotal.push({\n content: \"\",\n styles: {\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"bold\",\n },\n });\n }\n\n // Push baris GRAND TOTAL ke table\n tableRows.push(grandTotal);\n }\n\n if (typeof pdfSetting?.addRow === \"function\") {\n pdfSetting?.addRow(tableRows);\n }\n\n if (!pdfSetting?.disablePrintDate) {\n const totalColumns = countColumns(columns);\n\n tableRows.push([\n {\n content: `Print Date : ${convertDateTime(`${new Date()}`)}`,\n colSpan: totalColumns,\n styles: {\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n fontStyle: \"italic\",\n },\n },\n ]);\n }\n\n autoTable(doc, {\n head: [],\n body: tableRows,\n startY: finalY,\n theme: pdfSetting?.theme || \"plain\",\n rowPageBreak: \"avoid\",\n margin: { top: 10 },\n bodyStyles: { fontSize: pdfSetting?.fontSIze || 8 },\n headStyles: {\n fontSize: pdfSetting?.fontSIze || 8,\n textColor: `#${pdfSetting?.txtColor || \"000\"}`,\n fillColor: `#${pdfSetting?.bgColor || \"E8E5E5\"}`,\n },\n tableLineColor: [255, 255, 255],\n didParseCell: function (data) {\n const colIndex = data.column.index;\n const col = flatColumns[colIndex];\n const isImage = col?.options?.format === \"IMAGE\";\n\n if (isImage && data.cell.raw && (data.cell.raw as any).foto) {\n data.row.height = 20; // Ganti sesuai ukuran gambar\n data.cell.styles.valign = \"middle\"; // Ubah ke \"middle\" agar foto di tengah\n data.cell.styles.halign = \"center\"; // Tambahkan agar foto di tengah secara horizontal\n }\n },\n\n // ✅ Render gambar sesuai posisi dan ukuran\n didDrawCell: function (data) {\n const { cell } = data;\n const raw = cell.raw || {};\n const value = (raw as any).foto;\n const colIndex = data.column.index;\n const col = flatColumns[colIndex];\n\n if (col?.options?.format === \"IMAGE\" && value) {\n const imageSize = 15;\n const x = cell.x + (cell.width - imageSize) / 2;\n const y = cell.y + (cell.height - imageSize) / 2;\n\n try {\n doc.addImage(value, \"JPG\", x, y, imageSize, imageSize);\n } catch (err) {\n console.warn(\"❌ Gagal render gambar:\", err);\n }\n }\n }\n\n });\n tableRows = [];\n finalY = (doc as any).lastAutoTable.finalY;\n +3;\n\n const pages = (doc as any).internal.getNumberOfPages();\n const pageWidth = doc.internal.pageSize.width;\n const pageHeight = doc.internal.pageSize.height;\n\n doc.setFontSize(10);\n\n for (let j = 1; j < pages + 1; j++) {\n const horizontalPos = pageWidth / 2;\n const verticalPos = pageHeight - 10;\n doc.setPage(j);\n doc.text(`${j} of ${pages}`, horizontalPos, verticalPos, {\n align: \"center\",\n });\n }\n if (typeof pdfSetting?.customize === \"function\") {\n pdfSetting.customize(doc, finalY, autoTable);\n }\n\n if (pdfSetting?.openNewTab) {\n const blob = doc.output(\"bloburl\");\n window.open(blob);\n } else {\n doc.save(`${pdfSetting?.titlePdf || title}.pdf`);\n }\n};\n\nexport default ExportPDF;\n","interface Load {\n [key: string]: string | string[] | undefined;\n}\n\ninterface Res {\n data: Load[];\n template: string;\n copy?: boolean;\n}\n\nconst generateNotaSlip = (res: Res): string[] => {\n const notaGenerated: string[] = [];\n const jml = res.copy ? 2 : 1;\n for (let index = 0; index < jml; index++) {\n const nota: string[] = res.data.map((load: Load) => {\n let replaceLoop = res.template;\n\n while (/\\n!!LOOP\\((.+)\\)(\\{\\n(.*\\n)+\\})\\n/gm.exec(replaceLoop)) {\n replaceLoop = replaceLoop.replace(\n /\\n!!LOOP\\((.+)\\)(\\{\\n(.*\\n)+\\})\\n/,\n (_match, p1, p2) => {\n const loopContent = p2.replace(/^\\{/, \"\").replace(/\\}$/, \"\");\n const loopArray: string[] | undefined = !Array.isArray(load[p1])\n ? [load[p1] as string]\n : (load[p1] as string[]);\n const detail: string = loopArray.reduce((acc, val) => {\n return (\n acc +\n loopContent.replace(/\\{([a-z0-9_]+)\\}/gm, (c: any) => {\n const key = c.replace(/(\\{|\\})/g, \"\");\n if (key.match(/nama_barang/)) {\n const keyCustomer = key.match(/nama_barang/);\n const sliceNamaBarang =\n key === \"nama_barang2\"\n ? [20, 40]\n : key === \"nama_barang3\"\n ? [40, 60]\n : [0, 20];\n return (\n (val[keyCustomer] as string)\n ?.slice(...sliceNamaBarang)\n .trim() || \"\"\n );\n }\n if (key.match(/deskripsi_jual/)) {\n const keyDeskripsi = key.match(/deskripsi_jual/);\n const sliceDeskripsi =\n key === \"deskripsi_jual2\"\n ? [20, 40]\n : key === \"deskripsi_jual3\"\n ? [40, 60]\n : [0, 20];\n return (\n (val[keyDeskripsi] as string)\n ?.slice(...sliceDeskripsi)\n .trim() || \"\"\n );\n }\n if (key.match(/deskripsi/)) {\n const keyCustomer = key.match(/deskripsi/);\n const sliceNama =\n key === \"deskripsi2\"\n ? [20, 40]\n : key === \"deskripsi3\"\n ? [40, 60]\n : [0, 20];\n return (\n (val[keyCustomer] as string)\n ?.slice(...sliceNama)\n .trim() || \"\"\n );\n }\n return (val[key] as string) || \"\";\n })\n );\n }, \"\");\n return detail.replace(/\\n(\\s)+\\n/gm, \"\\n\");\n }\n );\n }\n\n return replaceLoop\n .replace(/\\{([a-z0-9_]+)\\}/gm, (c) => {\n const key = c.replace(/(\\{|\\})/g, \"\");\n\n if (key.match(/auto_cut/)) {\n return \"\\n\u001dVA\";\n }\n return (load[key] as string) || \"\";\n })\n .replace(/\\n(\\s)+\\n/gm, \"\\n\")\n .replace(/~new_line~/gm, \"\\n\")\n .replace(/!!LOOP\\(detail\\)/g, \"\")\n .replace(/[}{]/g, \"\");\n });\n\n for (const key in nota) {\n nota[key] += \"\\n\\n\";\n }\n\n notaGenerated.push(...nota);\n }\n\n return notaGenerated;\n};\n\nconst ExportToTxt = async (res: any, nama_file: string): Promise<void> => {\n const notaGenerated = generateNotaSlip(res);\n const blob = new Blob([notaGenerated?.join(\"\\n\") || \"\"], {\n type: \"text/plain\"\n });\n const downloadLink = document.createElement(\"a\");\n downloadLink.href = URL.createObjectURL(blob);\n downloadLink.download = nama_file;\n document.body.appendChild(downloadLink);\n downloadLink.click();\n document.body.removeChild(downloadLink);\n};\n\nexport default ExportToTxt;\n","import ExportExcel from \"./exportExcel\";\nimport ExportPDF from \"./exportPdf\";\nimport ExportToTxt from \"./exportTextFile\";\nimport { validateFileTypes } from \"./helpers\";\nimport { GenaratorExport, ColumnGenarator } from \"./interface\";\n\n/**\n * Ekspor ke PDF atau Excel berdasarkan konfigurasi yang diberikan.\n *\n * @param title - Judul laporan.\n * @param columns - Konfigurasi kolom untuk laporan.\n * @param data - Data yang akan disertakan dalam laporan.\n * @param grouping - Gruping yang akan diterapkan dalam laporan ada head dan detail Example: [\"no_faktur_hutang\"].\n * @param pdfSetting - Opsi untuk config PDF.\n * @param excelSetting - Opsi untuk config Excel.\n * @param txtSetting - Opsi untuk config Txt file.\n * @param date - Rentang tanggal untuk laporan.\n * @param type - Jenis laporan yang akan diekspor (\"PDF\" \"TXT\" atau \"EXCEL\").\n * @param footerSetting - Setting Footer Subtotal atau GranTotal\n */\nexport const ExportData = <T>({\n columns,\n data,\n grouping,\n date,\n type,\n txtSetting,\n pdfSetting,\n excelSetting,\n title,\n footerSetting\n}: GenaratorExport<T>): void => {\n const databaru = {\n data: [txtSetting?.dataTxt],\n template: txtSetting