diff --git a/package-lock.json b/package-lock.json index 58ea533a..3da74294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "localforage": "^1.10.0", "match-sorter": "^6.3.1", "moment": "^2.30.1", + "pdf-lib": "^1.17.1", "perfect-scrollbar": "^1.5.5", "react": "^18.2.0", "react-apexcharts": "^1.7.0", @@ -30,6 +31,7 @@ "react-router-dom": "^6.20.1", "react-toastify": "^11.0.2", "sort-by": "^1.2.0", + "xlsx": "^0.18.5", "zod": "^3.24.1" }, "devDependencies": { @@ -859,6 +861,24 @@ "node": ">= 8" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", @@ -1740,6 +1760,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2142,6 +2171,19 @@ } ] }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2176,6 +2218,15 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2223,6 +2274,18 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3028,6 +3091,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4234,6 +4306,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4279,6 +4357,18 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, "node_modules/perfect-scrollbar": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.6.tgz", @@ -5014,6 +5104,18 @@ "source-map": "^0.6.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", @@ -5217,6 +5319,12 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5640,6 +5748,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5655,6 +5781,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 6009a61d..9ef661c8 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "localforage": "^1.10.0", "match-sorter": "^6.3.1", "moment": "^2.30.1", + "pdf-lib": "^1.17.1", "perfect-scrollbar": "^1.5.5", "react": "^18.2.0", "react-apexcharts": "^1.7.0", @@ -33,6 +34,7 @@ "react-router-dom": "^6.20.1", "react-toastify": "^11.0.2", "sort-by": "^1.2.0", + "xlsx": "^0.18.5", "zod": "^3.24.1" }, "devDependencies": { diff --git a/src/utils/tableExportUtils.jsx b/src/utils/tableExportUtils.jsx new file mode 100644 index 00000000..90a1306a --- /dev/null +++ b/src/utils/tableExportUtils.jsx @@ -0,0 +1,186 @@ +import * as XLSX from "xlsx"; +import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; + +/** + * Export JSON data to CSV + * @param {Array} data - Array of objects to export + * @param {string} fileName - File name without extension + */ +export const exportToCSV = (data, fileName = "data") => { + const headers = Object.keys(data[0] || {}); + const csvContent = [ + headers.join(","), // header row + ...data.map(row => headers.map(field => `"${row[field] ?? ""}"`).join(",")), + ].join("\n"); + + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", `${fileName}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; + +/** + * Export JSON data to Excel + * @param {Array} data - Array of objects to export + * @param {string} fileName - File name without extension + */ +export const exportToExcel = (data, fileName = "data") => { + const ws = XLSX.utils.json_to_sheet(data); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, "Sheet1"); + XLSX.writeFile(wb, `${fileName}.xlsx`); +}; + +/** + * Export JSON data to PDF using pdf-lib + * @param {Array} data - Array of objects to export + * @param {string} fileName - File name for the PDF (optional) + */ +export const exportToPDF = async (data, fileName = "data") => { + if (!data || data.length === 0) return; + + // Create a new PDF document + const pdfDoc = await PDFDocument.create(); + + // Set up the font + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); // Use Helvetica font + + // Calculate column widths dynamically based on data content + const headers = Object.keys(data[0]); + const rows = data.map(item => headers.map(header => item[header] || '')); + + const getMaxColumnWidth = (columnIndex) => { + let maxWidth = font.widthOfTextAtSize(headers[columnIndex], 12); + rows.forEach(row => { + const cellText = row[columnIndex].toString(); + maxWidth = Math.max(maxWidth, font.widthOfTextAtSize(cellText, 10)); + }); + return maxWidth + 10; // Padding for better spacing + }; + + const columnWidths = headers.map((_, index) => getMaxColumnWidth(index)); + const tableX = 30; // X-coordinate for the table start + const rowHeight = 20; // Height of each row (can be adjusted) + const maxPageHeight = 750; // Max available height for content (before a new page is added) + const pageMargin = 30; // Margin from the top of the page + + let tableY = maxPageHeight; // Start Y position for the table + const maxPageWidth = 600; // Max available width for content (before a new page is added) + + // Add the headers and rows to the page + const addHeadersToPage = (page, scaleFactor) => { + let xPosition = tableX; + headers.forEach((header, index) => { + page.drawText(header, { + x: xPosition, + y: tableY, + font, + size: 12 * scaleFactor, // Scale the header font size + color: rgb(0, 0, 0), + }); + xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling + }); + tableY -= rowHeight; // Move down after adding headers + }; + + // Add a new page and reset the table position + const addNewPage = (scaleFactor) => { + const page = pdfDoc.addPage([600, 800]); + tableY = maxPageHeight; // Reset Y position for the new page + addHeadersToPage(page, scaleFactor); // Re-add headers to the new page + return page; + }; + + // Create the first page and add headers + let page = pdfDoc.addPage([600, 800]); + + // Check if the content fits within the page width, scale if necessary + const checkPageWidth = (row) => { + let totalWidth = columnWidths.reduce((acc, width) => acc + width, 0); + let scaleFactor = 1; + if (totalWidth > maxPageWidth) { + scaleFactor = maxPageWidth / totalWidth; // Scale down if necessary + } + + return scaleFactor; + }; + + // Function to check for page breaks when adding a new row + const checkPageBreak = () => { + if (tableY - rowHeight < pageMargin) { + page = addNewPage(scaleFactor); // Add a new page if there is no space for the next row + } + }; + + // Add rows to the PDF with pagination and horizontal scaling + rows.forEach(row => { + checkPageBreak(); // Check for page break before adding each row + + const scaleFactor = checkPageWidth(row); // Get the scaling factor for the row + + // Add headers to the first page and each new page with the same scale factor + if (tableY === maxPageHeight) { + addHeadersToPage(page, scaleFactor); // Add headers only on the first page + } + + let xPosition = tableX; + row.forEach((value, index) => { + page.drawText(value.toString(), { + x: xPosition, + y: tableY, + font, + size: 10 * scaleFactor, // Scale the font size + color: rgb(0, 0, 0), + }); + xPosition += columnWidths[index] * scaleFactor; // Adjust X position based on scaling + }); + + tableY -= rowHeight; // Move down to the next row position + }); + + // Serialize the document to bytes + const pdfBytes = await pdfDoc.save(); + + // Trigger a download of the PDF + const blob = new Blob([pdfBytes], { type: 'application/pdf' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${fileName}.pdf`; + link.click(); +}; + +/** + * Print the HTML table by accepting the table element or a reference. + * @param {HTMLElement} table - The table element (or ref) to print + */ +export const printTable = (table) => { + if (table) { + const newWindow = window.open("", "", "width=600,height=600"); // Open a new window + + // Inject styles for the table and body + newWindow.document.write("Print Table"); + const style = document.createElement('style'); + style.innerHTML = ` + body { font-family: Arial, sans-serif; } + table { border-collapse: collapse; width: 100%; } + th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } + th { background-color: #f2f2f2; } + `; + newWindow.document.head.appendChild(style); + + newWindow.document.write(""); + newWindow.document.write(table.outerHTML); // Write the table HTML to the new window + newWindow.document.write(""); + + newWindow.document.close(); // Close the document stream + + // Wait for the document to load before triggering print + newWindow.onload = () => { + newWindow.print(); // Trigger the print dialog after the content is loaded + }; + } +};