From 67ebb2ebd7b6c976bbd4b7985c62fe3cb2e4dc8e Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 23 Dec 2025 14:46:16 +0700 Subject: [PATCH] add excel pdf --- package-lock.json | 1076 ++++++++++++++++- package.json | 4 +- .../dashboards/daily-report/page.tsx | 569 +-------- src/utils/excelGenerator.ts | 525 ++++++++ src/utils/pdfGenerator.ts | 443 +++++++ .../daily-report/report-generator.tsx | 54 +- 6 files changed, 2119 insertions(+), 552 deletions(-) create mode 100644 src/utils/excelGenerator.ts create mode 100644 src/utils/pdfGenerator.ts diff --git a/package-lock.json b/package-lock.json index 91924f5..5029587 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "cmdk": "1.0.4", "date-fns": "4.1.0", "emoji-mart": "5.6.0", + "exceljs": "^4.4.0", "fs-extra": "11.2.0", "html2canvas": "^1.4.1", "input-otp": "1.4.1", @@ -78,7 +79,8 @@ "react-use": "17.6.0", "recharts": "2.15.0", "use-debounce": "^10.0.5", - "valibot": "0.42.1" + "valibot": "0.42.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@iconify/json": "2.2.286", @@ -1086,6 +1088,47 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -3963,6 +4006,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", @@ -4042,6 +4094,124 @@ "svg.select.js": "^3.0.1" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4282,6 +4452,12 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4423,7 +4599,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-arraybuffer": { @@ -4435,6 +4610,48 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4448,6 +4665,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4475,7 +4709,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4539,16 +4772,56 @@ "node": ">= 0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" } }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4687,6 +4960,31 @@ "node": ">=10.0.0" } }, + "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/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4860,6 +5158,15 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "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": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -4934,11 +5241,25 @@ "node": ">= 10" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/confbox": { @@ -4985,6 +5306,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -5001,6 +5328,31 @@ "node": ">=10" } }, + "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/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -5374,6 +5726,12 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -5607,6 +5965,51 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/earcut": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", @@ -5658,7 +6061,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -6563,6 +6965,26 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, "node_modules/extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -6596,6 +7018,19 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6934,6 +7369,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/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -6948,6 +7392,12 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -6992,7 +7442,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -7010,6 +7459,34 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7582,6 +8059,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", @@ -7623,7 +8106,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -7634,7 +8116,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -8350,6 +8831,54 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/kdbush": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", @@ -8416,6 +8945,54 @@ "node": ">=0.10" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8430,6 +9007,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -8458,6 +9044,12 @@ "uc.micro": "^2.0.0" } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/load-script": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", @@ -8503,6 +9095,73 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8517,6 +9176,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8957,7 +9628,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9112,7 +9782,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -9209,6 +9878,12 @@ "quansync": "^0.2.7" } }, + "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", @@ -9306,7 +9981,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9703,6 +10377,12 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10414,6 +11094,41 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -10628,6 +11343,62 @@ "node": ">= 0.8.15" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rope-sequence": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", @@ -10693,6 +11464,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -10735,6 +11526,18 @@ "dev": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -10863,6 +11666,12 @@ "node": ">=0.10.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -11165,6 +11974,18 @@ "node": ">=0.10.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/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -11249,6 +12070,15 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -12426,6 +13256,22 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -12512,6 +13358,15 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -12531,6 +13386,15 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "license": "MIT" }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -12815,6 +13679,60 @@ "node": ">= 10.0.0" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -12924,7 +13842,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/utrie": { @@ -12936,6 +13853,15 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/valibot": { "version": "0.42.1", "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.42.1.tgz", @@ -13117,6 +14043,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", @@ -13232,7 +14176,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -13249,6 +14192,33 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "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/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -13288,6 +14258,84 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } } } } diff --git a/package.json b/package.json index ae0cd89..a002c94 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "cmdk": "1.0.4", "date-fns": "4.1.0", "emoji-mart": "5.6.0", + "exceljs": "^4.4.0", "fs-extra": "11.2.0", "html2canvas": "^1.4.1", "input-otp": "1.4.1", @@ -84,7 +85,8 @@ "react-use": "17.6.0", "recharts": "2.15.0", "use-debounce": "^10.0.5", - "valibot": "0.42.1" + "valibot": "0.42.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@iconify/json": "2.2.286", diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx index 9c73a3a..935f441 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx @@ -8,6 +8,8 @@ import { useCategoryAnalytics } from '@/services/queries/analytics' import { useOutletById } from '@/services/queries/outlets' +import { generateExcel } from '@/utils/excelGenerator' +import { generatePDF } from '@/utils/pdfGenerator' import { formatCurrency, formatDate, formatDateDDMMYYYY, formatDatetime } from '@/utils/transform' import ReportGeneratorComponent from '@/views/dashboards/daily-report/report-generator' import ReportHeader from '@/views/dashboards/daily-report/report-header' @@ -16,6 +18,8 @@ import React, { useEffect, useRef, useState } from 'react' const DailyPOSReport = () => { const reportRef = useRef(null) + const [isGeneratingExcel, setIsGeneratingExcel] = useState(false) + const [now, setNow] = useState(new Date()) const [selectedDate, setSelectedDate] = useState(new Date()) const [dateRange, setDateRange] = useState({ @@ -112,536 +116,22 @@ const DailyPOSReport = () => { } const handleGeneratePDF = async () => { - const reportElement = reportRef.current - if (!reportElement) { - alert('Report element tidak ditemukan') - return - } - setIsGeneratingPDF(true) - try { - const jsPDF = (await import('jspdf')).default - const html2canvas = (await import('html2canvas')).default - const autoTable = (await import('jspdf-autotable')).default - - const pdf = new jsPDF({ - orientation: 'portrait', - unit: 'mm', - format: 'a4', - compress: true + await generatePDF({ + reportRef, + outlet, + profitLoss, + products, + paymentAnalytics, + category, + productSummary, + categorySummary, + filterType, + selectedDate, + dateRange, + now }) - - // Capture header section only (tanpa tabel kategori) - const headerElement = reportElement.querySelector('.report-header-section') as HTMLElement - if (headerElement) { - const headerCanvas = await html2canvas(headerElement, { - scale: 2, - useCORS: true, - backgroundColor: '#ffffff', - logging: false, - windowWidth: 794 - }) - - const headerImgWidth = 190 - const headerImgHeight = (headerCanvas.height * headerImgWidth) / headerCanvas.width - const headerImgData = headerCanvas.toDataURL('image/jpeg', 0.95) - - pdf.addImage(headerImgData, 'JPEG', 10, 10, headerImgWidth, headerImgHeight) - } - - let currentY = 80 // Start position after header - - // Add summary sections with autoTable - pdf.setFontSize(PDF_FONT_SIZES.heading) - pdf.setTextColor(54, 23, 94) - pdf.setFont('helvetica', 'bold') - pdf.text('Ringkasan', 14, currentY) - currentY += 15 - - // Summary table - TETAP PLAIN (TANPA BORDER) - autoTable(pdf, { - startY: currentY, - head: [], - body: [ - ['Total Penjualan', formatCurrency(profitLoss?.summary.total_revenue ?? 0)], - ['Total Diskon', formatCurrency(profitLoss?.summary.total_discount ?? 0)], - ['Total Pajak', formatCurrency(profitLoss?.summary.total_tax ?? 0)], - ['Total', formatCurrency(profitLoss?.summary.total_revenue ?? 0)] - ], - theme: 'grid', - styles: { - fontSize: PDF_FONT_SIZES.subheading, - cellPadding: PDF_SPACING.cellPadding, - lineColor: [0, 0, 0] - }, - columnStyles: { - 0: { fontStyle: 'normal', textColor: [60, 60, 60] }, - 1: { halign: 'right', fontStyle: 'bold', textColor: [60, 60, 60] } - }, - margin: { left: 14, right: 14 } - }) - - currentY = (pdf as any).lastAutoTable.finalY + 20 - - // Invoice section - TETAP PLAIN (TANPA BORDER) - pdf.setFontSize(PDF_FONT_SIZES.heading) - pdf.text('Invoice', 14, currentY) - currentY += 15 - - autoTable(pdf, { - startY: currentY, - head: [], - body: [['Total Invoice', String(profitLoss?.summary.total_orders ?? 0)]], - theme: 'grid', - styles: { - lineColor: [0, 0, 0], - fontSize: PDF_FONT_SIZES.subheading, - cellPadding: PDF_SPACING.cellPadding - }, - columnStyles: { - 0: { fontStyle: 'normal', textColor: [60, 60, 60] }, - 1: { halign: 'right', fontStyle: 'bold', textColor: [60, 60, 60] } - }, - margin: { left: 14, right: 14 } - }) - - pdf.addPage() - currentY = 20 - - // === TREN PENJUALAN (berdasarkan data sales analytics) === - // if (profitLoss7Days?.data && profitLoss7Days.data.length > 0) { - // pdf.setFontSize(PDF_FONT_SIZES.heading) - // pdf.text('Tren Penjualan 7 Hari Terakhir', 14, currentY) - // currentY += 10 - - // // Create canvas for chart - // const canvas = document.createElement('canvas') - // canvas.width = 800 - // canvas.height = 400 - // const ctx = canvas.getContext('2d') - - // if (ctx) { - // const { Chart, registerables } = await import('chart.js') - // Chart.register(...registerables) - - // // Prepare chart data - // const chartLabels = profitLoss7Days.data.map(day => - // new Date(day.date).toLocaleDateString('id-ID', { day: '2-digit', month: 'short' }) - // ) - // const revenueData = profitLoss7Days.data.map(day => day.revenue) - // const ordersData = profitLoss7Days.data.map(day => day.orders) - - // // Create chart - // new Chart(ctx, { - // type: 'bar', - // data: { - // labels: chartLabels, - // datasets: [ - // { - // label: 'Total Penjualan (Rp)', - // data: revenueData, - // backgroundColor: 'rgba(54, 23, 94, 0.8)', - // borderColor: 'rgba(54, 23, 94, 1)', - // borderWidth: 1, - // yAxisID: 'y' - // }, - // { - // label: 'Jumlah Invoice', - // data: ordersData, - // type: 'line', - // borderColor: 'rgba(59, 130, 246, 1)', - // backgroundColor: 'rgba(59, 130, 246, 0.1)', - // borderWidth: 2, - // tension: 0.4, - // yAxisID: 'y1', - // pointRadius: 4, - // pointBackgroundColor: 'rgba(59, 130, 246, 1)' - // } - // ] - // }, - // options: { - // responsive: false, - // animation: false, - // plugins: { - // legend: { - // display: true, - // position: 'top', - // labels: { - // font: { size: 12 }, - // padding: 15 - // } - // }, - // title: { - // display: false - // } - // }, - // scales: { - // y: { - // type: 'linear', - // position: 'left', - // beginAtZero: true, - // ticks: { - // callback: function (value) { - // if (typeof value !== 'number') return '' - // return 'Rp ' + (value / 1000).toFixed(0) + 'k' - // } - // }, - - // title: { - // display: true, - // text: 'Total Penjualan' - // } - // }, - // y1: { - // type: 'linear', - // position: 'right', - // beginAtZero: true, - // grid: { - // drawOnChartArea: false - // }, - // title: { - // display: true, - // text: 'Jumlah Invoice' - // } - // }, - // x: { - // grid: { - // display: false - // } - // } - // } - // } - // }) - - // // Wait for chart to render - // await new Promise(resolve => setTimeout(resolve, 500)) - - // // Convert chart to image - // const chartImage = canvas.toDataURL('image/png', 1.0) - // const chartWidth = 180 - // const chartHeight = 90 - - // pdf.addImage(chartImage, 'PNG', 15, currentY, chartWidth, chartHeight) - // currentY += chartHeight + 10 - - // // Add summary table below chart - // autoTable(pdf, { - // startY: currentY, - // head: [['Total Invoice', 'Total Penjualan']], - // body: [ - // [String(profitLoss7Days.summary.total_orders), formatCurrency(profitLoss7Days.summary.total_revenue)] - // ], - // theme: 'grid', - // styles: { - // fontSize: PDF_FONT_SIZES.tableContent, - // cellPadding: PDF_SPACING.cellPadding, - // lineColor: [0, 0, 0], - // lineWidth: 0.1, - // halign: 'center' - // }, - // headStyles: { - // fillColor: [54, 23, 94], - // textColor: 255, - // fontStyle: 'bold', - // fontSize: PDF_FONT_SIZES.tableHeader - // }, - // margin: { left: 14, right: 14 } - // }) - - // currentY = (pdf as any).lastAutoTable.finalY + 20 - // } - // } - - // Payment Method Summary - DENGAN BORDER HITAM - pdf.setFontSize(PDF_FONT_SIZES.heading) - pdf.text('Ringkasan Metode Pembayaran', 14, currentY) - currentY += 15 - - const paymentBody = - paymentAnalytics?.data?.map(payment => [ - payment.payment_method_name, - String(payment.order_count), - formatCurrency(payment.total_amount), - `${(payment.percentage ?? 0).toFixed(1)}%` - ]) || [] - - autoTable(pdf, { - startY: currentY, - head: [['Metode', 'Jumlah', 'Total', 'Persentase']], - body: paymentBody, - foot: [ - [ - 'TOTAL', - String(paymentAnalytics?.summary.total_orders ?? 0), - formatCurrency(paymentAnalytics?.summary.total_amount ?? 0), - '' - ] - ], - theme: 'grid', - styles: { - fontSize: PDF_FONT_SIZES.tableContent, - cellPadding: PDF_SPACING.cellPadding, - lineColor: [0, 0, 0], - lineWidth: 0.1 - }, - headStyles: { - fillColor: [54, 23, 94], - textColor: 255, - fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableHeader, - lineColor: [0, 0, 0], - lineWidth: 0.1 - }, - footStyles: { - fillColor: [220, 220, 220], - textColor: [60, 60, 60], - fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableFooter, - lineColor: [0, 0, 0], - lineWidth: 0.1, - halign: 'center' - }, - columnStyles: { - 1: { halign: 'center' }, - 2: { halign: 'right' }, - 3: { halign: 'right' } - }, - didParseCell: data => { - if (data.section === 'foot') { - if (data.column.index === 0) { - data.cell.styles.halign = 'left' - } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' - } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' - } - } - }, - margin: { left: 14, right: 14 } - }) - - currentY = (pdf as any).lastAutoTable.finalY + 20 - - // Category Summary - DENGAN BORDER HITAM - // Category Summary - DENGAN BORDER HITAM - pdf.setFontSize(PDF_FONT_SIZES.heading) - pdf.text('Ringkasan Kategori', 14, currentY) - currentY += 15 - - const categoryBody = - category?.data?.map(c => [c.category_name, String(c.total_quantity), formatCurrency(c.total_revenue)]) || [] - - autoTable(pdf, { - startY: currentY, - head: [['Nama', 'Qty', 'Pendapatan']], - body: categoryBody, - foot: [ - ['TOTAL', String(categorySummary?.totalQuantity ?? 0), formatCurrency(categorySummary?.totalRevenue ?? 0)] - ], - theme: 'grid', - // TAMBAHKAN INI: paksa semua row di satu page, jangan split - showFoot: 'lastPage', // footer cuma muncul di halaman terakhir - tableWidth: 'auto', - styles: { - fontSize: PDF_FONT_SIZES.tableContent, - cellPadding: PDF_SPACING.cellPadding, - lineColor: [0, 0, 0], - lineWidth: 0.1 - }, - headStyles: { - fillColor: [54, 23, 94], - textColor: 255, - fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableHeader, - lineColor: [0, 0, 0], - lineWidth: 0.1 - }, - footStyles: { - fillColor: [220, 220, 220], - textColor: [60, 60, 60], - fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableFooter, - lineColor: [0, 0, 0], - lineWidth: 0.1, - halign: 'center' - }, - columnStyles: { - 1: { halign: 'center' }, - 2: { halign: 'right' } - }, - didParseCell: data => { - if (data.section === 'foot') { - if (data.column.index === 0) { - data.cell.styles.halign = 'left' - } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' - } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' - } - } - }, - margin: { left: 14, right: 14 } - }) - - currentY = (pdf as any).lastAutoTable.finalY + 20 - - // Group products by category - const groupedProducts = - products?.data?.reduce( - (acc, item) => { - const categoryName = item.category_name || 'Tidak Berkategori' - if (!acc[categoryName]) { - acc[categoryName] = [] - } - acc[categoryName].push(item) - return acc - }, - {} as Record - ) || {} - - // Add new page for product details - pdf.addPage() - currentY = 20 - - pdf.setFontSize(PDF_FONT_SIZES.heading) - pdf.text('Ringkasan Item Per Kategori', 14, currentY) - currentY += 10 - - // Loop through each category - Object.keys(groupedProducts) - .sort((a, b) => { - const productsA = groupedProducts[a] - const productsB = groupedProducts[b] - const orderA = productsA[0]?.category_order ?? 999 - const orderB = productsB[0]?.category_order ?? 999 - return orderA - orderB - }) - .forEach((categoryName, index) => { - const categoryProducts = groupedProducts[categoryName].sort((a, b) => { - const skuA = a.product_sku || '' - const skuB = b.product_sku || '' - return skuA.localeCompare(skuB) - }) - - const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0) - const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0) - - const productBody = categoryProducts.map(item => [ - item.product_name, - String(item.quantity_sold), - formatCurrency(item.revenue) - ]) - - const estimatedHeight = (productBody.length + 3) * 12 - if (currentY + estimatedHeight > 270) { - pdf.addPage() - currentY = 20 - } - - pdf.setFontSize(PDF_FONT_SIZES.subheading) - pdf.setFont('helvetica', 'bold') - pdf.setTextColor(54, 23, 94) - pdf.text(`${index + 1}. ${categoryName.toUpperCase()}`, 16, currentY) - currentY += 15 - - autoTable(pdf, { - startY: currentY, - head: [['Produk', 'Qty', 'Pendapatan']], - body: productBody, - foot: [[`Subtotal ${categoryName}`, String(categoryTotalQty), formatCurrency(categoryTotalRevenue)]], - showFoot: 'lastPage', // ← TAMBAHKAN INI: subtotal cuma muncul di akhir kategori - theme: 'grid', - styles: { - fontSize: PDF_FONT_SIZES.tableContent, - cellPadding: PDF_SPACING.cellPadding, - lineColor: [0, 0, 0], - lineWidth: 0.1 - }, - headStyles: { - fillColor: [54, 23, 94], - textColor: 255, - fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableHeader, - lineColor: [0, 0, 0], - lineWidth: 0.1 - }, - footStyles: { - fillColor: [200, 200, 200], - textColor: [60, 60, 60], - fontStyle: 'bold', - fontSize: PDF_FONT_SIZES.tableFooter, - lineColor: [0, 0, 0], - lineWidth: 0.1, - halign: 'center' - }, - columnStyles: { - 0: { cellWidth: 90 }, - 1: { halign: 'center', cellWidth: 40 }, - 2: { halign: 'right', cellWidth: 52 } - }, - didParseCell: data => { - if (data.section === 'foot') { - if (data.column.index === 0) { - data.cell.styles.halign = 'left' - } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' - } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' - } - } - }, - margin: { left: 14, right: 14 } - }) - - currentY = (pdf as any).lastAutoTable.finalY + 15 - }) - - // Grand Total - DENGAN BORDER HITAM - if (currentY > 250) { - pdf.addPage() - currentY = 20 - } - - autoTable(pdf, { - startY: currentY, - head: [], - body: [ - ['TOTAL KESELURUHAN', String(productSummary.totalQuantitySold), formatCurrency(productSummary.totalRevenue)] - ], - theme: 'grid', - styles: { - fontSize: PDF_FONT_SIZES.grandTotal, - cellPadding: 6, - fontStyle: 'bold', - textColor: [54, 23, 94], - lineColor: [0, 0, 0], - lineWidth: 0.2 - }, - columnStyles: { - 0: { cellWidth: 90 }, - 1: { halign: 'center', cellWidth: 40 }, - 2: { halign: 'right', cellWidth: 52 } - }, - margin: { left: 14, right: 14 } - }) - - // Footer - const pageCount = pdf.getNumberOfPages() - for (let i = 1; i <= pageCount; i++) { - pdf.setPage(i) - pdf.setFontSize(11) - pdf.setTextColor(120, 120, 120) - pdf.text('© 2025 Apskel - Sistem POS Terpadu', 14, 287) - pdf.text(`Dicetak pada: ${now.toLocaleDateString('id-ID')}`, 190, 287, { align: 'right' }) - } - - const fileName = - filterType === 'single' - ? `laporan-transaksi-${formatDateForInput(selectedDate)}.pdf` - : `laporan-transaksi-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.pdf` - - pdf.save(fileName) } catch (error) { console.error('Error generating PDF:', error) alert(`Terjadi kesalahan saat membuat PDF: ${error}`) @@ -650,6 +140,29 @@ const DailyPOSReport = () => { } } + const handleGenerateExcel = async () => { + setIsGeneratingExcel(true) + try { + await generateExcel({ + outlet, + profitLoss, + products, + paymentAnalytics, + category, + productSummary, + categorySummary, + filterType, + selectedDate, + dateRange + }) + } catch (error) { + console.error('Error generating Excel:', error) + alert(`Terjadi kesalahan saat membuat Excel: ${error}`) + } finally { + setIsGeneratingExcel(false) + } + } + const LoadingOverlay = ({ isVisible, message = 'Generating PDF...' }: { isVisible: boolean; message?: string }) => { if (!isVisible) return null @@ -707,6 +220,8 @@ const DailyPOSReport = () => { onSingleDateChange={setSelectedDate} onDateRangeChange={setDateRange} onGeneratePDF={handleGeneratePDF} + onGenerateExcel={handleGenerateExcel} + isGeneratingExcel={isGeneratingExcel} />
diff --git a/src/utils/excelGenerator.ts b/src/utils/excelGenerator.ts new file mode 100644 index 0000000..ce9042b --- /dev/null +++ b/src/utils/excelGenerator.ts @@ -0,0 +1,525 @@ +// excelGenerator.ts +import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform' +import type ExcelJS from 'exceljs' + +interface ExcelGeneratorParams { + outlet: any + profitLoss: any + products: any + paymentAnalytics: any + category: any + productSummary: { + totalQuantitySold: number + totalRevenue: number + totalOrders: number + } + categorySummary: { + totalRevenue: number + orderCount: number + productCount: number + totalQuantity: number + } + filterType: 'single' | 'range' + selectedDate: Date + dateRange: { startDate: Date; endDate: Date } +} + +const formatDateForInput = (date: Date) => { + return date.toISOString().split('T')[0] +} + +const getReportPeriodText = (params: ExcelGeneratorParams) => { + if (params.filterType === 'single') { + return `${formatDateDDMMYYYY(params.selectedDate)} - ${formatDateDDMMYYYY(params.selectedDate)}` + } + return `${formatDateDDMMYYYY(params.dateRange.startDate)} - ${formatDateDDMMYYYY(params.dateRange.endDate)}` +} + +// ========== EXCEL STYLES (IMPROVED) ========== +const headerStyle: Partial = { + font: { bold: true, size: 12, color: { argb: 'FFFFFFFF' } }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern, + alignment: { vertical: 'middle', horizontal: 'center' }, + border: { + top: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + left: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + bottom: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + right: { style: 'thin', color: { argb: 'FFD1D5DB' } } + } +} + +const titleStyle: Partial = { + font: { bold: true, size: 18 }, + alignment: { vertical: 'middle', horizontal: 'left' } +} + +const subtitleStyle: Partial = { + font: { size: 11, color: { argb: 'FF6B7280' } }, + alignment: { vertical: 'middle', horizontal: 'left' } +} + +const labelStyle: Partial = { + font: { bold: true, size: 11 }, + alignment: { vertical: 'middle', horizontal: 'left' } +} + +const totalRowStyle: Partial = { + font: { bold: true, size: 11 }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern, + border: { + top: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + left: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + bottom: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + right: { style: 'thin', color: { argb: 'FFD1D5DB' } } + } +} + +const dataStyle: Partial = { + border: { + top: { style: 'thin', color: { argb: 'FFF3F4F6' } }, + left: { style: 'thin', color: { argb: 'FFF3F4F6' } }, + bottom: { style: 'thin', color: { argb: 'FFF3F4F6' } }, + right: { style: 'thin', color: { argb: 'FFF3F4F6' } } + } +} + +// Zebra striping untuk data rows +const dataStyleAlt: Partial = { + ...dataStyle, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFAFAFA' } } as ExcelJS.FillPattern +} + +const currencyFormat = '#,##0' +const percentageFormat = '0.0"%"' + +export const generateExcel = async (params: ExcelGeneratorParams) => { + const { + outlet, + profitLoss, + products, + paymentAnalytics, + category, + productSummary, + categorySummary, + filterType, + selectedDate, + dateRange + } = params + + const ExcelJS = await import('exceljs') + const workbook = new ExcelJS.Workbook() + + // Metadata + workbook.creator = outlet?.name || 'POS System' + workbook.created = new Date() + + // ============ SHEET 1: RINGKASAN ============ + const ws1 = workbook.addWorksheet('Ringkasan', { + views: [{ showGridLines: false }] + }) + + // Title + ws1.mergeCells('A1:B1') + const titleCell = ws1.getCell('A1') + titleCell.value = 'LAPORAN TRANSAKSI' + titleCell.style = { + font: { bold: true, size: 20, color: { argb: 'FF1F2937' } }, + alignment: { vertical: 'middle', horizontal: 'left' } + } + ws1.getRow(1).height = 30 + + // Outlet Info + ws1.getCell('A2').value = outlet?.name || '' + ws1.getCell('A2').style = { ...subtitleStyle, font: { size: 12, color: { argb: 'FF4B5563' } } } + ws1.getCell('A3').value = outlet?.address || '' + ws1.getCell('A3').style = subtitleStyle + ws1.getCell('A4').value = outlet?.phone || '' + ws1.getCell('A4').style = subtitleStyle + + ws1.getCell('A5').value = 'Periode:' + ws1.getCell('B5').value = getReportPeriodText(params) + ws1.getCell('A5').style = labelStyle + ws1.getCell('B5').style = { font: { size: 11 } } + + // Section: Ringkasan + ws1.getCell('A7').value = 'RINGKASAN' + ws1.getCell('A7').style = { + font: { bold: true, size: 14, color: { argb: 'FF36175E' } }, + alignment: { vertical: 'middle', horizontal: 'left' } + } + + // Header row + ws1.getCell('A8').value = 'Keterangan' + ws1.getCell('B8').value = 'Jumlah (IDR)' + ws1.getCell('A8').style = headerStyle + ws1.getCell('B8').style = headerStyle + ws1.getRow(8).height = 25 + + // Data rows + const summaryData = [ + ['Total Penjualan', profitLoss?.summary.total_revenue || 0], + ['Total Diskon', profitLoss?.summary.total_discount || 0], + ['Total Pajak', profitLoss?.summary.total_tax || 0], + ['PB1', 0], + ['Service Charge', 0] + ] + + summaryData.forEach((row, idx) => { + const rowNum = idx + 9 + ws1.getCell(`A${rowNum}`).value = row[0] + ws1.getCell(`B${rowNum}`).value = row[1] + + const isAlt = idx % 2 === 1 + const baseStyle = isAlt ? dataStyleAlt : dataStyle + + ws1.getCell(`A${rowNum}`).style = { ...baseStyle, alignment: { horizontal: 'left' } } + ws1.getCell(`B${rowNum}`).style = { + ...baseStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat + } + + ws1.getRow(rowNum).height = 22 + }) + + // Total row + const totalRow = 9 + summaryData.length + ws1.getCell(`A${totalRow}`).value = 'Total' + ws1.getCell(`B${totalRow}`).value = profitLoss?.summary.total_revenue || 0 + ws1.getCell(`A${totalRow}`).style = { ...totalRowStyle, alignment: { horizontal: 'left' } } + ws1.getCell(`B${totalRow}`).style = { + ...totalRowStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat + } + ws1.getRow(totalRow).height = 25 + + // Invoice section (pisah dari tabel ringkasan) + const invoiceRow = totalRow + 2 + ws1.getCell(`A${invoiceRow}`).value = 'INVOICE' + ws1.getCell(`A${invoiceRow}`).style = { + font: { bold: true, size: 14, color: { argb: 'FF36175E' } }, + alignment: { vertical: 'middle', horizontal: 'left' } + } + + ws1.getCell(`A${invoiceRow + 1}`).value = 'Total Invoice' + ws1.getCell(`B${invoiceRow + 1}`).value = profitLoss?.summary.total_orders || 0 + ws1.getCell(`A${invoiceRow + 1}`).style = { ...dataStyle, font: { bold: true } } + ws1.getCell(`B${invoiceRow + 1}`).style = { ...dataStyle, alignment: { horizontal: 'right' }, font: { bold: true } } + + // Column widths + ws1.getColumn(1).width = 28 + ws1.getColumn(2).width = 22 + + // ============ SHEET 2: METODE PEMBAYARAN ============ + const ws2 = workbook.addWorksheet('Metode Pembayaran', { + views: [{ showGridLines: false }] + }) + + // Title + ws2.mergeCells('A1:D1') + const paymentTitle = ws2.getCell('A1') + paymentTitle.value = 'RINGKASAN METODE PEMBAYARAN' + paymentTitle.style = titleStyle + ws2.getRow(1).height = 30 + + // Headers + const paymentHeaders = ['Metode', 'Jumlah Order', 'Total Amount (IDR)', 'Persentase'] + paymentHeaders.forEach((header, idx) => { + const cell = ws2.getCell(3, idx + 1) + cell.value = header + cell.style = headerStyle + }) + ws2.getRow(3).height = 25 + + // Data + let paymentRow = 4 + paymentAnalytics?.data?.forEach((payment: any, idx: number) => { + const isAlt = idx % 2 === 1 + const baseStyle = isAlt ? dataStyleAlt : dataStyle + + ws2.getCell(paymentRow, 1).value = payment.payment_method_name + ws2.getCell(paymentRow, 2).value = payment.order_count + ws2.getCell(paymentRow, 3).value = payment.total_amount + ws2.getCell(paymentRow, 4).value = (payment.percentage || 0) / 100 + + ws2.getCell(paymentRow, 1).style = { ...baseStyle, alignment: { horizontal: 'left' } } + ws2.getCell(paymentRow, 2).style = { ...baseStyle, alignment: { horizontal: 'center' } } + ws2.getCell(paymentRow, 3).style = { + ...baseStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat + } + ws2.getCell(paymentRow, 4).style = { + ...baseStyle, + alignment: { horizontal: 'center' }, + numFmt: percentageFormat + } + + ws2.getRow(paymentRow).height = 22 + paymentRow++ + }) + + // Total row + ws2.getCell(paymentRow, 1).value = 'TOTAL' + ws2.getCell(paymentRow, 2).value = paymentAnalytics?.summary.total_orders || 0 + ws2.getCell(paymentRow, 3).value = paymentAnalytics?.summary.total_amount || 0 + ws2.getCell(paymentRow, 4).value = '' + + ws2.getCell(paymentRow, 1).style = { ...totalRowStyle, alignment: { horizontal: 'left' } } + ws2.getCell(paymentRow, 2).style = { ...totalRowStyle, alignment: { horizontal: 'center' } } + ws2.getCell(paymentRow, 3).style = { + ...totalRowStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat + } + ws2.getCell(paymentRow, 4).style = totalRowStyle + ws2.getRow(paymentRow).height = 25 + + // Column widths + ws2.getColumn(1).width = 28 + ws2.getColumn(2).width = 16 + ws2.getColumn(3).width = 22 + ws2.getColumn(4).width = 16 + + // ============ SHEET 3: KATEGORI ============ + const ws3 = workbook.addWorksheet('Kategori', { + views: [{ showGridLines: false }] + }) + + // Title + ws3.mergeCells('A1:C1') + const categoryTitle = ws3.getCell('A1') + categoryTitle.value = 'RINGKASAN KATEGORI' + categoryTitle.style = titleStyle + ws3.getRow(1).height = 30 + + // Headers + const categoryHeaders = ['Nama', 'Qty', 'Pendapatan (IDR)'] + categoryHeaders.forEach((header, idx) => { + const cell = ws3.getCell(3, idx + 1) + cell.value = header + cell.style = headerStyle + }) + ws3.getRow(3).height = 25 + + // Data + let categoryRow = 4 + category?.data?.forEach((cat: any, idx: number) => { + const isAlt = idx % 2 === 1 + const baseStyle = isAlt ? dataStyleAlt : dataStyle + + ws3.getCell(categoryRow, 1).value = cat.category_name + ws3.getCell(categoryRow, 2).value = cat.total_quantity + ws3.getCell(categoryRow, 3).value = cat.total_revenue + + ws3.getCell(categoryRow, 1).style = { ...baseStyle, alignment: { horizontal: 'left' } } + ws3.getCell(categoryRow, 2).style = { ...baseStyle, alignment: { horizontal: 'center' } } + ws3.getCell(categoryRow, 3).style = { + ...baseStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat + } + + ws3.getRow(categoryRow).height = 22 + categoryRow++ + }) + + // Total row + ws3.getCell(categoryRow, 1).value = 'TOTAL' + ws3.getCell(categoryRow, 2).value = categorySummary.totalQuantity + ws3.getCell(categoryRow, 3).value = categorySummary.totalRevenue + + ws3.getCell(categoryRow, 1).style = { ...totalRowStyle, alignment: { horizontal: 'left' } } + ws3.getCell(categoryRow, 2).style = { ...totalRowStyle, alignment: { horizontal: 'center' } } + ws3.getCell(categoryRow, 3).style = { + ...totalRowStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat + } + ws3.getRow(categoryRow).height = 25 + + // Column widths + ws3.getColumn(1).width = 35 + ws3.getColumn(2).width = 12 + ws3.getColumn(3).width = 22 + + // ============ SHEET 4: DETAIL PRODUK ============ + const ws4 = workbook.addWorksheet('Detail Produk', { + views: [{ showGridLines: false }] + }) + + // Title + ws4.mergeCells('A1:C1') + const productTitle = ws4.getCell('A1') + productTitle.value = 'RINGKASAN ITEM PER KATEGORI' + productTitle.style = titleStyle + ws4.getRow(1).height = 30 + + // Group products by category + const groupedProducts = + products?.data?.reduce( + (acc: any, item: any) => { + const categoryName = item.category_name || 'Tidak Berkategori' + if (!acc[categoryName]) { + acc[categoryName] = [] + } + acc[categoryName].push(item) + return acc + }, + {} as Record + ) || {} + + let currentRow = 3 + + // Loop through each category + Object.keys(groupedProducts) + .sort((a, b) => { + const productsA = groupedProducts[a] + const productsB = groupedProducts[b] + const orderA = productsA[0]?.category_order ?? 999 + const orderB = productsB[0]?.category_order ?? 999 + return orderA - orderB + }) + .forEach((categoryName, index) => { + // Category header + ws4.mergeCells(`A${currentRow}:C${currentRow}`) + const catHeader = ws4.getCell(`A${currentRow}`) + catHeader.value = `${index + 1}. ${categoryName.toUpperCase()}` + catHeader.style = { + font: { bold: true, size: 13, color: { argb: 'FF36175E' } }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF9FAFB' } } as ExcelJS.FillPattern, + alignment: { vertical: 'middle', horizontal: 'left' }, + border: { + top: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + left: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + bottom: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + right: { style: 'thin', color: { argb: 'FFD1D5DB' } } + } + } + ws4.getRow(currentRow).height = 28 + currentRow++ + + // Column headers + const prodHeaders = ['Produk', 'Qty', 'Pendapatan (IDR)'] + prodHeaders.forEach((header, idx) => { + const cell = ws4.getCell(currentRow, idx + 1) + cell.value = header + cell.style = { + font: { bold: true, size: 11, color: { argb: 'FFFFFFFF' } }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern, + alignment: { vertical: 'middle', horizontal: 'center' }, + border: { + top: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + left: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + bottom: { style: 'thin', color: { argb: 'FFD1D5DB' } }, + right: { style: 'thin', color: { argb: 'FFD1D5DB' } } + } + } + }) + ws4.getRow(currentRow).height = 24 + currentRow++ + + // Sort products + const categoryProducts = groupedProducts[categoryName].sort((a: any, b: any) => { + const skuA = a.product_sku || '' + const skuB = b.product_sku || '' + return skuA.localeCompare(skuB) + }) + + // Add products with zebra striping + categoryProducts.forEach((product: any, idx: number) => { + const isAlt = idx % 2 === 1 + const baseStyle = isAlt ? dataStyleAlt : dataStyle + + ws4.getCell(currentRow, 1).value = product.product_name + ws4.getCell(currentRow, 2).value = product.quantity_sold + ws4.getCell(currentRow, 3).value = product.revenue + + ws4.getCell(currentRow, 1).style = { ...baseStyle, alignment: { horizontal: 'left' } } + ws4.getCell(currentRow, 2).style = { ...baseStyle, alignment: { horizontal: 'center' } } + ws4.getCell(currentRow, 3).style = { + ...baseStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat + } + + ws4.getRow(currentRow).height = 20 + currentRow++ + }) + + // Subtotal + const subQty = categoryProducts.reduce((sum: number, p: any) => sum + p.quantity_sold, 0) + const subRevenue = categoryProducts.reduce((sum: number, p: any) => sum + p.revenue, 0) + + ws4.getCell(currentRow, 1).value = `Subtotal ${categoryName}` + ws4.getCell(currentRow, 2).value = subQty + ws4.getCell(currentRow, 3).value = subRevenue + + ws4.getCell(currentRow, 1).style = { + ...totalRowStyle, + alignment: { horizontal: 'left' }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern + } + ws4.getCell(currentRow, 2).style = { + ...totalRowStyle, + alignment: { horizontal: 'center' }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern + } + ws4.getCell(currentRow, 3).style = { + ...totalRowStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF3F4F6' } } as ExcelJS.FillPattern + } + + ws4.getRow(currentRow).height = 24 + currentRow += 3 // Spacing lebih lega + }) + + // Grand Total + ws4.getCell(currentRow, 1).value = 'TOTAL KESELURUHAN' + ws4.getCell(currentRow, 2).value = productSummary.totalQuantitySold + ws4.getCell(currentRow, 3).value = productSummary.totalRevenue + + ws4.getCell(currentRow, 1).style = { + ...totalRowStyle, + alignment: { horizontal: 'left' }, + font: { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern + } + ws4.getCell(currentRow, 2).style = { + ...totalRowStyle, + alignment: { horizontal: 'center' }, + font: { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern + } + ws4.getCell(currentRow, 3).style = { + ...totalRowStyle, + alignment: { horizontal: 'right' }, + numFmt: currencyFormat, + font: { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }, + fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF36175E' } } as ExcelJS.FillPattern + } + ws4.getRow(currentRow).height = 28 + + // Column widths + ws4.getColumn(1).width = 45 + ws4.getColumn(2).width = 12 + ws4.getColumn(3).width = 22 + + // ============ GENERATE & DOWNLOAD FILE ============ + const fileName = + filterType === 'single' + ? `laporan-transaksi-${formatDateForInput(selectedDate)}.xlsx` + : `laporan-transaksi-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.xlsx` + + const buffer = await workbook.xlsx.writeBuffer() + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = fileName + a.click() + window.URL.revokeObjectURL(url) +} diff --git a/src/utils/pdfGenerator.ts b/src/utils/pdfGenerator.ts new file mode 100644 index 0000000..c435ca0 --- /dev/null +++ b/src/utils/pdfGenerator.ts @@ -0,0 +1,443 @@ +// pdfGenerator.ts +import { formatCurrency } from '@/utils/transform' + +// PDF Configuration +const PDF_FONT_SIZES = { + heading: 18, + subheading: 16, + tableContent: 12, + tableHeader: 12, + tableFooter: 12, + grandTotal: 14, + footer: 11 +} + +const PDF_SPACING = { + cellPadding: 5, + cellPaddingLarge: 6, + sectionGap: 20, + headerGap: 15 +} + +interface PDFGeneratorParams { + reportRef: React.RefObject + outlet: any + profitLoss: any + products: any + paymentAnalytics: any + category: any + productSummary: { + totalQuantitySold: number + totalRevenue: number + totalOrders: number + } + categorySummary: { + totalRevenue: number + orderCount: number + productCount: number + totalQuantity: number + } + filterType: 'single' | 'range' + selectedDate: Date + dateRange: { startDate: Date; endDate: Date } + now: Date +} + +const formatDateForInput = (date: Date) => { + return date.toISOString().split('T')[0] +} + +export const generatePDF = async (params: PDFGeneratorParams) => { + const { + reportRef, + outlet, + profitLoss, + products, + paymentAnalytics, + category, + productSummary, + categorySummary, + filterType, + selectedDate, + dateRange, + now + } = params + + const reportElement = reportRef.current + if (!reportElement) { + throw new Error('Report element tidak ditemukan') + } + + // Dynamic imports + const jsPDF = (await import('jspdf')).default + const html2canvas = (await import('html2canvas')).default + const autoTable = (await import('jspdf-autotable')).default + + const pdf = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4', + compress: true + }) + + // Capture header section + const headerElement = reportElement.querySelector('.report-header-section') as HTMLElement + if (headerElement) { + const headerCanvas = await html2canvas(headerElement, { + scale: 2, + useCORS: true, + backgroundColor: '#ffffff', + logging: false, + windowWidth: 794 + }) + + const headerImgWidth = 190 + const headerImgHeight = (headerCanvas.height * headerImgWidth) / headerCanvas.width + const headerImgData = headerCanvas.toDataURL('image/jpeg', 0.95) + + pdf.addImage(headerImgData, 'JPEG', 10, 10, headerImgWidth, headerImgHeight) + } + + let currentY = 80 + + // ========== RINGKASAN SECTION ========== + pdf.setFontSize(PDF_FONT_SIZES.heading) + pdf.setTextColor(54, 23, 94) + pdf.setFont('helvetica', 'bold') + pdf.text('Ringkasan', 14, currentY) + currentY += 15 + + autoTable(pdf, { + startY: currentY, + head: [], + body: [ + ['Total Penjualan', formatCurrency(profitLoss?.summary.total_revenue ?? 0)], + ['Total Diskon', formatCurrency(profitLoss?.summary.total_discount ?? 0)], + ['Total Pajak', formatCurrency(profitLoss?.summary.total_tax ?? 0)], + ['Total', formatCurrency(profitLoss?.summary.total_revenue ?? 0)] + ], + theme: 'grid', + styles: { + fontSize: PDF_FONT_SIZES.subheading, + cellPadding: PDF_SPACING.cellPadding, + lineColor: [0, 0, 0] + }, + columnStyles: { + 0: { fontStyle: 'normal', textColor: [60, 60, 60] }, + 1: { halign: 'right', fontStyle: 'bold', textColor: [60, 60, 60] } + }, + margin: { left: 14, right: 14 } + }) + + currentY = (pdf as any).lastAutoTable.finalY + 20 + + // ========== INVOICE SECTION ========== + pdf.setFontSize(PDF_FONT_SIZES.heading) + pdf.text('Invoice', 14, currentY) + currentY += 15 + + autoTable(pdf, { + startY: currentY, + head: [], + body: [['Total Invoice', String(profitLoss?.summary.total_orders ?? 0)]], + theme: 'grid', + styles: { + lineColor: [0, 0, 0], + fontSize: PDF_FONT_SIZES.subheading, + cellPadding: PDF_SPACING.cellPadding + }, + columnStyles: { + 0: { fontStyle: 'normal', textColor: [60, 60, 60] }, + 1: { halign: 'right', fontStyle: 'bold', textColor: [60, 60, 60] } + }, + margin: { left: 14, right: 14 } + }) + + pdf.addPage() + currentY = 20 + + // ========== PAYMENT METHOD SECTION ========== + pdf.setFontSize(PDF_FONT_SIZES.heading) + pdf.text('Ringkasan Metode Pembayaran', 14, currentY) + currentY += 15 + + const paymentBody = + paymentAnalytics?.data?.map((payment: any) => [ + payment.payment_method_name, + String(payment.order_count), + formatCurrency(payment.total_amount), + `${(payment.percentage ?? 0).toFixed(1)}%` + ]) || [] + + autoTable(pdf, { + startY: currentY, + head: [['Metode', 'Jumlah', 'Total', 'Persentase']], + body: paymentBody, + foot: [ + [ + 'TOTAL', + String(paymentAnalytics?.summary.total_orders ?? 0), + formatCurrency(paymentAnalytics?.summary.total_amount ?? 0), + '' + ] + ], + theme: 'grid', + styles: { + fontSize: PDF_FONT_SIZES.tableContent, + cellPadding: PDF_SPACING.cellPadding, + lineColor: [0, 0, 0], + lineWidth: 0.1 + }, + headStyles: { + fillColor: [54, 23, 94], + textColor: 255, + fontStyle: 'bold', + fontSize: PDF_FONT_SIZES.tableHeader, + lineColor: [0, 0, 0], + lineWidth: 0.1 + }, + footStyles: { + fillColor: [220, 220, 220], + textColor: [60, 60, 60], + fontStyle: 'bold', + fontSize: PDF_FONT_SIZES.tableFooter, + lineColor: [0, 0, 0], + lineWidth: 0.1, + halign: 'center' + }, + columnStyles: { + 1: { halign: 'center' }, + 2: { halign: 'right' }, + 3: { halign: 'right' } + }, + didParseCell: (data: any) => { + if (data.section === 'foot') { + if (data.column.index === 0) { + data.cell.styles.halign = 'left' + } else if (data.column.index === 1) { + data.cell.styles.halign = 'center' + } else if (data.column.index === 2) { + data.cell.styles.halign = 'right' + } + } + }, + margin: { left: 14, right: 14 } + }) + + currentY = (pdf as any).lastAutoTable.finalY + 20 + + // ========== CATEGORY SECTION ========== + pdf.setFontSize(PDF_FONT_SIZES.heading) + pdf.text('Ringkasan Kategori', 14, currentY) + currentY += 15 + + const categoryBody = + category?.data?.map((c: any) => [c.category_name, String(c.total_quantity), formatCurrency(c.total_revenue)]) || [] + + autoTable(pdf, { + startY: currentY, + head: [['Nama', 'Qty', 'Pendapatan']], + body: categoryBody, + foot: [['TOTAL', String(categorySummary?.totalQuantity ?? 0), formatCurrency(categorySummary?.totalRevenue ?? 0)]], + theme: 'grid', + showFoot: 'lastPage', + tableWidth: 'auto', + styles: { + fontSize: PDF_FONT_SIZES.tableContent, + cellPadding: PDF_SPACING.cellPadding, + lineColor: [0, 0, 0], + lineWidth: 0.1 + }, + headStyles: { + fillColor: [54, 23, 94], + textColor: 255, + fontStyle: 'bold', + fontSize: PDF_FONT_SIZES.tableHeader, + lineColor: [0, 0, 0], + lineWidth: 0.1 + }, + footStyles: { + fillColor: [220, 220, 220], + textColor: [60, 60, 60], + fontStyle: 'bold', + fontSize: PDF_FONT_SIZES.tableFooter, + lineColor: [0, 0, 0], + lineWidth: 0.1, + halign: 'center' + }, + columnStyles: { + 1: { halign: 'center' }, + 2: { halign: 'right' } + }, + didParseCell: (data: any) => { + if (data.section === 'foot') { + if (data.column.index === 0) { + data.cell.styles.halign = 'left' + } else if (data.column.index === 1) { + data.cell.styles.halign = 'center' + } else if (data.column.index === 2) { + data.cell.styles.halign = 'right' + } + } + }, + margin: { left: 14, right: 14 } + }) + + currentY = (pdf as any).lastAutoTable.finalY + 20 + + // ========== PRODUCT DETAILS BY CATEGORY ========== + const groupedProducts = + products?.data?.reduce( + (acc: any, item: any) => { + const categoryName = item.category_name || 'Tidak Berkategori' + if (!acc[categoryName]) { + acc[categoryName] = [] + } + acc[categoryName].push(item) + return acc + }, + {} as Record + ) || {} + + pdf.addPage() + currentY = 20 + + pdf.setFontSize(PDF_FONT_SIZES.heading) + pdf.text('Ringkasan Item Per Kategori', 14, currentY) + currentY += 10 + + Object.keys(groupedProducts) + .sort((a, b) => { + const productsA = groupedProducts[a] + const productsB = groupedProducts[b] + const orderA = productsA[0]?.category_order ?? 999 + const orderB = productsB[0]?.category_order ?? 999 + return orderA - orderB + }) + .forEach((categoryName, index) => { + const categoryProducts = groupedProducts[categoryName].sort((a: any, b: any) => { + const skuA = a.product_sku || '' + const skuB = b.product_sku || '' + return skuA.localeCompare(skuB) + }) + + const categoryTotalQty = categoryProducts.reduce((sum: number, item: any) => sum + (item.quantity_sold || 0), 0) + const categoryTotalRevenue = categoryProducts.reduce((sum: number, item: any) => sum + (item.revenue || 0), 0) + + const productBody = categoryProducts.map((item: any) => [ + item.product_name, + String(item.quantity_sold), + formatCurrency(item.revenue) + ]) + + const estimatedHeight = (productBody.length + 3) * 12 + if (currentY + estimatedHeight > 270) { + pdf.addPage() + currentY = 20 + } + + pdf.setFontSize(PDF_FONT_SIZES.subheading) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(54, 23, 94) + pdf.text(`${index + 1}. ${categoryName.toUpperCase()}`, 16, currentY) + currentY += 15 + + autoTable(pdf, { + startY: currentY, + head: [['Produk', 'Qty', 'Pendapatan']], + body: productBody, + foot: [[`Subtotal ${categoryName}`, String(categoryTotalQty), formatCurrency(categoryTotalRevenue)]], + showFoot: 'lastPage', + theme: 'grid', + styles: { + fontSize: PDF_FONT_SIZES.tableContent, + cellPadding: PDF_SPACING.cellPadding, + lineColor: [0, 0, 0], + lineWidth: 0.1 + }, + headStyles: { + fillColor: [54, 23, 94], + textColor: 255, + fontStyle: 'bold', + fontSize: PDF_FONT_SIZES.tableHeader, + lineColor: [0, 0, 0], + lineWidth: 0.1 + }, + footStyles: { + fillColor: [200, 200, 200], + textColor: [60, 60, 60], + fontStyle: 'bold', + fontSize: PDF_FONT_SIZES.tableFooter, + lineColor: [0, 0, 0], + lineWidth: 0.1, + halign: 'center' + }, + columnStyles: { + 0: { cellWidth: 90 }, + 1: { halign: 'center', cellWidth: 40 }, + 2: { halign: 'right', cellWidth: 52 } + }, + didParseCell: (data: any) => { + if (data.section === 'foot') { + if (data.column.index === 0) { + data.cell.styles.halign = 'left' + } else if (data.column.index === 1) { + data.cell.styles.halign = 'center' + } else if (data.column.index === 2) { + data.cell.styles.halign = 'right' + } + } + }, + margin: { left: 14, right: 14 } + }) + + currentY = (pdf as any).lastAutoTable.finalY + 15 + }) + + // ========== GRAND TOTAL ========== + if (currentY > 250) { + pdf.addPage() + currentY = 20 + } + + autoTable(pdf, { + startY: currentY, + head: [], + body: [ + ['TOTAL KESELURUHAN', String(productSummary.totalQuantitySold), formatCurrency(productSummary.totalRevenue)] + ], + theme: 'grid', + styles: { + fontSize: PDF_FONT_SIZES.grandTotal, + cellPadding: 6, + fontStyle: 'bold', + textColor: [54, 23, 94], + lineColor: [0, 0, 0], + lineWidth: 0.2 + }, + columnStyles: { + 0: { cellWidth: 90 }, + 1: { halign: 'center', cellWidth: 40 }, + 2: { halign: 'right', cellWidth: 52 } + }, + margin: { left: 14, right: 14 } + }) + + // ========== FOOTER ========== + const pageCount = pdf.getNumberOfPages() + for (let i = 1; i <= pageCount; i++) { + pdf.setPage(i) + pdf.setFontSize(11) + pdf.setTextColor(120, 120, 120) + pdf.text('© 2025 Apskel - Sistem POS Terpadu', 14, 287) + pdf.text(`Dicetak pada: ${now.toLocaleDateString('id-ID')}`, 190, 287, { align: 'right' }) + } + + // ========== SAVE PDF ========== + const fileName = + filterType === 'single' + ? `laporan-transaksi-${formatDateForInput(selectedDate)}.pdf` + : `laporan-transaksi-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.pdf` + + pdf.save(fileName) +} diff --git a/src/views/dashboards/daily-report/report-generator.tsx b/src/views/dashboards/daily-report/report-generator.tsx index 9dbcfde..4e84f2f 100644 --- a/src/views/dashboards/daily-report/report-generator.tsx +++ b/src/views/dashboards/daily-report/report-generator.tsx @@ -59,6 +59,7 @@ interface ReportGeneratorProps { onSingleDateChange: (date: Date) => void onDateRangeChange: (dateRange: DateRange) => void onGeneratePDF: () => void + onGenerateExcel: () => void // Optional props dengan default values maxWidth?: string @@ -66,8 +67,10 @@ interface ReportGeneratorProps { customQuickActions?: CustomQuickActions | null periodFormat?: string downloadButtonText?: string + downloadExcelButtonText?: string cardShadow?: string primaryColor?: string + excelButtonColor?: string // Optional helper functions formatDateForInput?: ((date: Date) => string) | null @@ -80,6 +83,7 @@ interface ReportGeneratorProps { // Loading state isGenerating?: boolean + isGeneratingExcel?: boolean // Custom labels labels?: Labels @@ -110,6 +114,22 @@ const PurpleButton = styled(Button)(({ theme }) => ({ } })) +const GreenButton = styled(Button)(({ theme }) => ({ + backgroundColor: '#16a34a', + color: 'white', + textTransform: 'none', + fontWeight: 500, + padding: '8px 24px', + '&:hover': { + backgroundColor: '#15803d', + opacity: 0.9 + }, + '&:disabled': { + backgroundColor: theme.palette.mode === 'dark' ? '#444' : '#ccc', + color: theme.palette.mode === 'dark' ? '#888' : '#666' + } +})) + const QuickActionButton = styled(Button)(({ theme }) => ({ backgroundColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.08)' : '#f8f7fa', color: theme.palette.mode === 'dark' ? theme.palette.text.secondary : '#6f6b7d', @@ -140,6 +160,7 @@ const ReportGeneratorComponent: React.FC = ({ onSingleDateChange, onDateRangeChange, onGeneratePDF, + onGenerateExcel, // Optional props dengan default values maxWidth = '1024px', @@ -147,8 +168,10 @@ const ReportGeneratorComponent: React.FC = ({ customQuickActions = null, periodFormat = 'id-ID', downloadButtonText = 'Download PDF', + downloadExcelButtonText = 'Download Excel', cardShadow, primaryColor = '#36175e', + excelButtonColor = '#16a34a', // Optional helper functions formatDateForInput = null, @@ -161,6 +184,7 @@ const ReportGeneratorComponent: React.FC = ({ // Loading state isGenerating = false, + isGeneratingExcel = false, // Custom labels labels = { @@ -175,7 +199,7 @@ const ReportGeneratorComponent: React.FC = ({ last7DaysLabel: '7 Hari Terakhir', last30DaysLabel: '30 Hari Terakhir', periodLabel: 'Periode:', - exportHelpText: 'Klik tombol download untuk mengeksport laporan ke PDF' + exportHelpText: 'Klik tombol download untuk mengeksport laporan ke PDF atau Excel' } }) => { const theme = useTheme() @@ -271,14 +295,24 @@ const ReportGeneratorComponent: React.FC = ({ } action={ - - {isGenerating ? 'Generating...' : downloadButtonText} - + + + {isGenerating ? 'Generating...' : downloadButtonText} + + + {isGeneratingExcel ? 'Generating...' : downloadExcelButtonText} + + } sx={{ pb: 2 }} /> @@ -505,7 +539,7 @@ const ReportGeneratorComponent: React.FC = ({ mt: 1 }} > - {labels.exportHelpText || 'Klik tombol download untuk mengeksport laporan ke PDF'} + {labels.exportHelpText || 'Klik tombol download untuk mengeksport laporan ke PDF atau Excel'}