exportdatafile
Version:
Export Data Excel Pdf and Txt
1 lines • 91.5 kB
Source Map (JSON)
{"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\" | \"DATE\" | \"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 txtColor?: string;\n bgColor?: string;\n width?: number;\n disabledColumn?: boolean;\n disabledFooter?: boolean;\n };\n child?: ColumnGenarator<T>[];\n formatter?: (cellValue: any, rowData: any) => any;\n}\n\nexport interface DataItemGenerator {\n [key: string]: any;\n}\n\nexport type FileType = \"EXCEL\" | \"PDF\" | \"TXT\" | \"ALL\";\ntype GroupingStyle = {\n txtColor?: string;\n bgColor?: string;\n halign?: \"left\" | \"right\" | \"center\";\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 = (\n worksheet: ExcelJS.Worksheet,\n lastIndex: number\n) => void;\ntype GroupingSettingType =\n | GroupingStyle\n | ((item: DataItemGenerator) => GroupingStyle);\nexport interface GenaratorExport<T> {\n columns: ColumnGenarator<T>[];\n data: DataItemGenerator[];\n type: FileType[];\n title?: string;\n groupingSetting?: GroupingSettingType;\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 startY?: number;\n header?: {\n column?: boolean;\n information?: boolean;\n };\n textHeaderRight?: string;\n textHeaderLeft?: string;\n theme?: \"grid\" | \"striped\" | \"plain\";\n grandTotalSetting?: {\n disableGrandTotal?: boolean;\n colSpan?: number;\n };\n openNewTab?: boolean;\n disablePrintDate?: boolean;\n returnDataUri?(dataUri: string): void;\n addRow?: addRowPdfPdfFunction;\n // customize?: CustomizePdfFunction;\n customHeader?: CustomizePdfFunction;\n customFooter?: CustomizePdfFunction;\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 startY?: number;\n txtColor?: string;\n additionalTextHeader?: string;\n grandTotalSetting?: {\n disableGrandTotal?: boolean;\n colSpan?: number;\n };\n subTotal?: {\n disableGrandTotal?: boolean;\n };\n returnBuffer?(buffer: string): void;\n customHeader?: CustomizeFunctionExcel;\n customFooter?: CustomizeFunctionExcel;\n };\n grouping: string[];\n footerSetting?: {\n subTotal?: {\n caption?: string;\n disableSubtotal?: boolean;\n enableCount?: boolean;\n captionItem?: string;\n };\n grandTotal?: {\n caption?: string;\n disableGrandTotal?: boolean;\n captionItem?: string;\n enableCount?: boolean;\n };\n };\n}\n","import ExcelJS from \"exceljs\";\nimport { ColumnGenarator, FileType, validFileTypes } from \"./interface\";\n\nexport function convertDateTime(tgl: string, isDateTime: boolean = false) {\n const date = new Date(tgl);\n\n // Tambahkan offset 7 jam (WIB)\n const jakartaDate = new Date(date.getTime() + 7 * 60 * 60 * 1000);\n\n const year = jakartaDate.getUTCFullYear();\n const month = String(jakartaDate.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(jakartaDate.getUTCDate()).padStart(2, \"0\");\n const hours = String(jakartaDate.getUTCHours()).padStart(2, \"0\");\n const minutes = String(jakartaDate.getUTCMinutes()).padStart(2, \"0\");\n const seconds = String(jakartaDate.getUTCSeconds()).padStart(2, \"0\");\n\n return isDateTime\n ? `${day}-${month}-${year} ${hours}:${minutes}:${seconds}`\n : `${day}-${month}-${year}`;\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 * 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(\n cellData.imageSrc\n );\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): {\n buffer: Uint8Array;\n extension: any;\n} {\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}\n\nexport const convertArgbToRgb = (color: string): string => {\n if (!color) return color;\n\n // Remove # if present\n const cleanColor = color.replace(/^#/, \"\");\n\n // If 8 characters (ARGB), strip first 2 characters (alpha channel)\n if (cleanColor.length === 8) {\n return cleanColor.substring(2);\n }\n\n // If already 6 characters (RGB) or 3 characters (short RGB), return as is\n return cleanColor;\n};\n\nexport function bufferToBase64(buffer: ArrayBuffer, fileName: string): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n const base64 = btoa(binary);\n return `data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;filename=${fileName};base64,${base64}`;\n}\n","import {\n addImagesToRow,\n bufferToBase64,\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 groupingSetting,\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 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 \"\n } : ${date?.start_date} ${date?.end_date ? `s/d ${date?.end_date}` : \"\"}`;\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 if (excelSetting?.customHeader) {\n excelSetting.customHeader(worksheet, lastUsedColumnIndex);\n }\n\n const startY =\n excelSetting?.startY && excelSetting.startY > 0 ? excelSetting.startY : 1;\n\n if (startY > 1) {\n for (let i = 1; i < startY; i++) {\n worksheet.addRow([]);\n }\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\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 // Set column widths based on col.options?.width\n // This implementation supports both parent and child columns\n // Usage example:\n // {\n // label: \"Column Name\",\n // key: \"column_key\",\n // options: {\n // width: 25, // Set column width in Excel units (approximate character count)\n // halign: \"center\"\n // }\n // }\n const flatColumns = getFlattenColumns(columns);\n flatColumns.forEach((col, index) => {\n const columnIndex = index + 1; // ExcelJS uses 1-based indexing\n const column = worksheet.getColumn(columnIndex);\n\n if (col.options?.width) {\n // Use specified width from column options\n column.width = col.options.width;\n } else {\n // Set default width based on column type for better readability\n const defaultWidth = col.options?.format === \"IMAGE\" ? 15 : 20;\n column.width = defaultWidth;\n }\n });\n\n const totals: { [key: string]: number } = {};\n\n data.forEach(async (item) => {\n if (item.detail?.length > 0) {\n if (grouping.length > 0) {\n const totalColumns = countColumns(columns); // sudah kamu punya\n const groupContent = grouping\n .map((column) =>\n item[column] !== undefined || item[column] !== null\n ? `${formatingTitle(column)} : ${item[column]}`\n : \"\"\n )\n .filter(Boolean)\n .join(\" | \");\n\n // Support groupingSetting sebagai function atau object\n const groupingStyle =\n typeof groupingSetting === \"function\"\n ? groupingSetting(item)\n : groupingSetting || {};\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)}${\n groupRow.number\n }`\n );\n // Styling\n groupRow.getCell(1).alignment = {\n horizontal: groupingStyle?.halign || \"left\",\n };\n\n if (groupingStyle?.txtColor) {\n groupRow.getCell(1).font = {\n bold: true,\n color: {\n argb: groupingStyle?.txtColor || \"000000\",\n },\n };\n }\n\n if (groupingStyle?.bgColor) {\n groupRow.getCell(1).fill = {\n type: \"pattern\",\n pattern: \"solid\",\n fgColor: {\n argb: groupingStyle?.bgColor,\n },\n };\n }\n }\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\n let value =\n column?.options?.format === \"DATETIME\"\n ? convertDateTime(\n itemDetail[column.key as keyof DataItemGenerator],\n true\n )\n : column?.options?.format === \"DATE\"\n ? convertDateTime(\n itemDetail[column.key as keyof DataItemGenerator]\n )\n : itemDetail[column.key as keyof DataItemGenerator];\n\n // Apply formatter if exists\n if (column.formatter) {\n value = column.formatter(value, itemDetail);\n }\n\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\n // Hitung totals dan subtotals menggunakan nilai original (sebelum formatter)\n const originalValue =\n itemDetail[column.key as keyof DataItemGenerator];\n totals[columnKey] = (totals[columnKey] || 0) + Number(originalValue);\n subtotal[columnKey] =\n (subtotal[columnKey] || 0) + Number(originalValue);\n\n return {\n value,\n alignment,\n ...(column?.options?.bgColor && {\n fgColor: { argb: column.options.bgColor },\n }),\n ...(column?.options?.txtColor && {\n color: { argb: column.options.txtColor },\n }),\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<\n | \"center\"\n | \"right\"\n | \"left\"\n | \"fill\"\n | \"justify\"\n | \"centerContinuous\"\n | \"distributed\"\n > = [\n \"center\",\n \"right\",\n \"left\",\n \"fill\",\n \"justify\",\n \"centerContinuous\",\n \"distributed\",\n ];\n const allowedVertical: Array<\n \"top\" | \"middle\" | \"bottom\" | \"distributed\" | \"justify\"\n > = [\"top\", \"middle\", \"bottom\", \"distributed\", \"justify\"];\n\n let horizontal: (typeof allowedHorizontal)[number] | undefined =\n undefined;\n let vertical: (typeof allowedVertical)[number] | undefined =\n undefined;\n\n if (\n cellData.alignment &&\n typeof cellData.alignment.horizontal === \"string\" &&\n allowedHorizontal.includes(cellData.alignment.horizontal as any)\n ) {\n horizontal = cellData.alignment\n .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)\n .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.isImage &&\n \"numFmt\" in cellData &&\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\"\n } ${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 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 } 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 let value =\n column?.options?.format === \"DATETIME\"\n ? convertDateTime(item[column.key as keyof DataItemGenerator])\n : column?.options?.format === \"DATE\"\n ? convertDateTime(item[column.key as keyof DataItemGenerator])\n : item[column.key as keyof DataItemGenerator];\n\n // Apply formatter if exists\n if (column.formatter) {\n value = column.formatter(value, item);\n }\n\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\n // Hitung totals menggunakan nilai original (sebelum formatter)\n const originalValue = item[column.key as keyof DataItemGenerator];\n totals[columnKey] = (totals[columnKey] || 0) + Number(originalValue);\n\n return {\n value,\n alignment,\n ...(column?.options?.bgColor && {\n fgColor: { argb: column.options.bgColor },\n }),\n ...(column?.options?.txtColor && {\n color: { argb: column.options.txtColor },\n }),\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<\n | \"center\"\n | \"right\"\n | \"left\"\n | \"fill\"\n | \"justify\"\n | \"centerContinuous\"\n | \"distributed\"\n > = [\n \"center\",\n \"right\",\n \"left\",\n \"fill\",\n \"justify\",\n \"centerContinuous\",\n \"distributed\",\n ];\n const allowedVertical: Array<\n \"top\" | \"middle\" | \"bottom\" | \"distributed\" | \"justify\"\n > = [\"top\", \"middle\", \"bottom\", \"distributed\", \"justify\"];\n\n let horizontal: (typeof allowedHorizontal)[number] | undefined =\n undefined;\n let vertical: (typeof allowedVertical)[number] | undefined = undefined;\n\n if (\n cellData.alignment &&\n typeof cellData.alignment.horizontal === \"string\" &&\n allowedHorizontal.includes(cellData.alignment.horizontal as any)\n ) {\n horizontal = cellData.alignment\n .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)\n .vertical as (typeof allowedVertical)[number];\n }\n\n cell.alignment = {\n ...(horizontal ? { horizontal } : {}),\n ...(vertical ? { vertical } : {}),\n };\n\n if (\n !cellData.isImage &&\n \"numFmt\" in cellData &&\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 if (!cellData.isImage && \"fgColor\" in cellData && cellData.fgColor) {\n cell.fill = {\n type: \"pattern\",\n pattern: \"solid\",\n fgColor: cellData.fgColor,\n };\n }\n if (!cellData.isImage && \"color\" in cellData && cellData.color) {\n cell.font = {\n color: cellData.color,\n };\n }\n });\n\n // Add images after all cell formatting is complete\n await addImagesToRow(workbook, worksheet, row, rowData);\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 flatColumnsTott.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 = 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 = `${\n footerSetting?.grandTotal?.caption || \"GRAND TOTAL\"\n } ${GrandTotal} ${caption}`;\n grandTotalRow.getCell(1).value = footerGrandtotal;\n grandTotalRow.getCell(1).alignment = { horizontal: \"center\" };\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 if (excelSetting?.customFooter) {\n excelSetting.customFooter(worksheet, lastUsedColumnIndex);\n }\n\n const buffer = await workbook.xlsx.writeBuffer();\n const fileName = `${excelSetting?.titleExcel || title}.xlsx`;\n\n const base64DataURI = bufferToBase64(buffer, fileName);\n\n if (excelSetting?.returnBuffer) {\n excelSetting.returnBuffer(base64DataURI);\n }\n\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};\nexport default ExportExcel;\n","import {\n ColumnGenarator,\n DataItemGenerator,\n GenaratorExport,\n} from \"./interface\";\nimport {\n convertArgbToRgb,\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 groupingSetting,\n}: GenaratorExport<T>): string | 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} ${\n date?.end_date ? `s/d ${date?.end_date}` : \"\"\n }`,\n widthPortrait - 15,\n 22,\n { align: \"right\" }\n );\n }\n\n if (typeof pdfSetting?.customHeader === \"function\") {\n pdfSetting.customHeader(doc, finalY, autoTable);\n }\n doc.setProperties({\n title: title || pdfSetting?.titlePdf,\n });\n\n if (pdfSetting?.startY) {\n // console.log(pdfSetting.finalY);\n finalY = pdfSetting.startY;\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: `#${convertArgbToRgb(pdfSetting?.txtColor || \"000\")}`,\n fillColor: `#${convertArgbToRgb(pdfSetting?.bgColor || \"E8E5E5\")}`,\n // maxWidth: column?.options?.width || 0,\n ...(column?.options?.width && {\n cellWidth: column?.options?.width,\n }),\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 hanya jika header.column tidak aktif\n if (!pdfSetting?.header?.column) {\n tableRows.push(headerRow1);\n if (hasChild && headerRow2.length > 0) {\n tableRows.push(headerRow2);\n }\n }\n\n // Body Tabel\n const totals: { [key: string]: number } = {};\n\n data.forEach((item) => {\n if (item.detail?.length > 0) {\n // Tambah row grup (header kelompok)\n\n if (grouping.length > 0) {\n const totalColumns = countColumns(columns);\n const groupContent = grouping\n .map((column) =>\n item[column] !== undefined || item[column] !== null\n ? `${formatingTitle(column)} : ${item[column]}`\n : \"\"\n )\n .filter(Boolean)\n .join(\" | \");\n\n // Support groupingSetting sebagai function atau object\n const groupingStyle =\n typeof groupingSetting === \"function\"\n ? groupingSetting(item)\n : groupingSetting || {};\n\n const groupRow = [\n {\n content: groupContent,\n colSpan: totalColumns,\n styles: {\n fontStyle: \"bold\",\n halign: groupingStyle?.halign || \"left\",\n textColor: `#${convertArgbToRgb(\n groupingStyle?.txtColor || \"000\"\n )}`,\n fillColor: `#${convertArgbToRgb(\n groupingStyle?.bgColor || \"FFF\"\n )}`,\n },\n },\n ];\n\n tableRows.push(groupRow);\n }\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 let value = list2[column.key as keyof DataItemGenerator];\n const columnKey = column.key as keyof DataItemGenerator;\n\n // Apply formatter if exists\n if (column.formatter) {\n value = column.formatter(value, list2);\n }\n\n // Hitung subtotal & total (gunakan nilai original, bukan formatted)\n const originalValue = list2[column.key as keyof DataItemGenerator];\n totals[columnKey] =\n (totals[columnKey] || 0) + Number(originalValue || 0);\n subtotal[columnKey] =\n (subtotal[columnKey] || 0) + Number(originalValue || 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(), true)\n : \"\";\n case \"DATE\":\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 textColor: column?.options?.txtColor\n ? `#${convertArgbToRgb(column?.options?.txtColor)}`\n : \"#000\",\n fillColor: column?.options?.bgColor\n ? `#${convertArgbToRgb(column?.options?.bgColor)}`\n : undefined,\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 tableRows.push(rowData);\n });\n\n if (!footerSetting?.subTotal?.disableSubtotal) {\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: `#${convertArgbToRgb(\n pdfSetting?.txtColor || \"000\"\n )}`,\n fillColor: `#${convertArgbToRgb(\n pdfSetting?.bgColor || \"E8E5E5\"\n )}`,\n fontStyle: \"bold\",\n },\n });\n } else {\n footersubtotal.push({\n content: \"\",\n styles: {\n textColor: `#${convertArgbToRgb(\n pdfSetting?.txtColor || \"000\"\n )}`,\n fillColor: `#${convertArgbToRgb(\n pdfSetting?.bgColor || \"E8E5E5\"\n )}`,\n fontStyle: \"bold\",\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: `${\n footerSetting?.subTotal?.caption || \"SUB TOTAL\"\n }${subtotalCount} ${captionSub}`,\n colSpan,\n styles: {\n textColor: `#${convertArgbToRgb(pdfSetting?.txtColor || \"000\")}`,\n fillColor: `#${convertArgbToRgb(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 let value = item[column.key as keyof DataItemGenerator];\n const columnKey = column.key as keyof DataItemGenerator;\n\n // Apply formatter if exists\n if (column.formatter) {\n value = column.formatter(value, item);\n }\n\n // Hitung total jika tidak disabled (gunakan nilai original, bukan formatted)\n if (!column.options?.disabledFooter) {\n const originalValue = item[column.key as keyof DataItemGenerator];\n totals[columnKey] =\n (totals[columnKey] || 0) + Number(originalValue || 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(), true)\n : \"\";\n case \"DATE\":\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 return {\n options: column?.options,\n content,\n foto: isImage ? value : null,\n styles: {\n halign,\n textColor: column?.options?.txtColor\n ? `#${convertArgbToRgb(column?.options?.txtColor)}`\n : \"#000\",\n fillColor: column?.options?.bgColor\n ? `#${convertArgbToRgb(column?.options?.bgColor)}`\n : undefined,\n },\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: `#${convertArgbToRgb(pdfSetting?.txtColor || \"000\")}`,\n fillColor: `#${convertArgbToRgb(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 c