diff --git a/package-lock.json b/package-lock.json index 295394c..f5fa89b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "date-fns": "^4.1.0", "echarts": "^6.1.0", "echarts-for-react": "^3.0.6", + "exceljs": "^4.4.0", "html-to-image": "^1.11.13", "jwt-decode": "^4.0.0", "lucide-react": "^0.563.0", @@ -1314,6 +1315,47 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.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.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", @@ -7857,6 +7899,102 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=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/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/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/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -8082,6 +8220,12 @@ "dev": true, "license": "MIT" }, + "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", @@ -8132,7 +8276,26 @@ "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-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/baseline-browser-mapping": { @@ -8144,11 +8307,49 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "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/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/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==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8202,6 +8403,56 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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==", + "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/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -8322,6 +8573,18 @@ "node": ">=18" } }, + "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", @@ -8440,11 +8703,25 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "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/convert-source-map": { @@ -8468,6 +8745,37 @@ "url": "https://opencollective.com/express" } }, + "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/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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8782,6 +9090,12 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -8960,6 +9274,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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -9011,6 +9370,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "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==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -9726,6 +10094,38 @@ "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/exceljs/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/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -9736,6 +10136,19 @@ "node": ">=12.0.0" } }, + "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", @@ -9909,6 +10322,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -9924,6 +10349,22 @@ "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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -10181,7 +10622,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphql": { @@ -10379,6 +10819,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -10389,6 +10849,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/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10426,6 +10892,23 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.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.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -11122,6 +11605,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/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -11161,6 +11692,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", @@ -11175,6 +11754,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/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -11436,6 +12024,12 @@ "url": "https://opencollective.com/parcel" } }, + "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/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11464,6 +12058,73 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "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", @@ -11471,6 +12132,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/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -11624,7 +12297,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -11637,7 +12309,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11653,6 +12324,18 @@ "node": ">=16 || 14 >=14.17" } }, + "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/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -11897,6 +12580,15 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -12026,6 +12718,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -12108,6 +12809,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "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", @@ -12144,6 +12851,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -12359,6 +13075,12 @@ "dev": true, "license": "MIT" }, + "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", @@ -12803,6 +13525,50 @@ "react-dom": ">=16.6.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/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -12967,6 +13733,40 @@ "node": ">=0.10.0" } }, + "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/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -13063,6 +13863,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", @@ -13183,6 +14003,12 @@ "node": ">= 0.4" } }, + "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.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -13452,6 +14278,15 @@ "dev": true, "license": "MIT" }, + "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": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -13790,6 +14625,22 @@ "url": "https://opencollective.com/webpack" } }, + "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/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -13949,6 +14800,15 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "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", @@ -13998,6 +14858,15 @@ "node": ">=18" } }, + "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": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -14304,6 +15173,60 @@ "url": "https://github.com/sponsors/kettanaito" } }, + "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.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -14397,6 +15320,22 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -14863,6 +15802,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -14899,7 +15844,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, "license": "MIT" }, "node_modules/y18n": { @@ -14971,6 +15915,62 @@ "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/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index d09c986..f7137dd 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "date-fns": "^4.1.0", "echarts": "^6.1.0", "echarts-for-react": "^3.0.6", + "exceljs": "^4.4.0", "html-to-image": "^1.11.13", "jwt-decode": "^4.0.0", "lucide-react": "^0.563.0", diff --git a/src/app/(dashboard)/finance/import-jobs/import-jobs-page-client.tsx b/src/app/(dashboard)/finance/import-jobs/import-jobs-page-client.tsx new file mode 100644 index 0000000..e4d7cc5 --- /dev/null +++ b/src/app/(dashboard)/finance/import-jobs/import-jobs-page-client.tsx @@ -0,0 +1,288 @@ +"use client" + +import { useState } from "react" +import { + Download, + RefreshCw, + FileSpreadsheet, + Clock, + CheckCircle2, + XCircle, + AlertTriangle, + Loader2, +} from "lucide-react" +import { format, parseISO } from "date-fns" + +import { PageHeader } from "@/components/common/page-header" +import { EmptyState } from "@/components/common/empty-state" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Progress } from "@/components/ui/progress" +import { DataTablePagination } from "@/components/shared" +import { useImportJobs } from "@/hooks/finance/use-cost-import" +import { IMPORT_ENTITY_LABELS } from "@/types/finance/cost-import" +import type { CostImportJob, ImportJobStatus } from "@/types/finance/cost-import" + +const ALL = "all" + +const STATUS_OPTIONS = [ + { value: ALL, label: "Semua Status" }, + { value: "PENDING", label: "Pending" }, + { value: "RUNNING", label: "Running" }, + { value: "DONE", label: "Done" }, + { value: "PARTIAL", label: "Partial" }, + { value: "FAILED", label: "Failed" }, +] + +const ENTITY_OPTIONS = [ + { value: ALL, label: "Semua Tipe" }, + { value: "product_master", label: "Product Master" }, + { value: "capp", label: "Cost Applicable Parameters" }, + { value: "cpp", label: "Cost Product Parameters" }, + { value: "bulk_product_routing", label: "Bulk Import (Product + Routing)" }, + { value: "bulk_product_routing_export", label: "Bulk Export (Product + Routing)" }, +] + +function StatusIcon({ status }: { status: ImportJobStatus }) { + switch (status) { + case "DONE": + return + case "FAILED": + return + case "PARTIAL": + return + case "RUNNING": + return + default: + return + } +} + +function StatusBadgeJob({ status }: { status: ImportJobStatus }) { + const variants: Record = { + PENDING: { variant: "secondary", label: "Pending" }, + RUNNING: { variant: "default", label: "Running" }, + DONE: { variant: "default", label: "Done" }, + PARTIAL: { variant: "outline", label: "Partial" }, + FAILED: { variant: "destructive", label: "Failed" }, + } + const { variant, label } = variants[status] ?? { variant: "secondary", label: status } + const colorClass = + status === "DONE" + ? "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-400" + : status === "PARTIAL" + ? "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400" + : status === "RUNNING" + ? "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400" + : "" + return ( + + {label} + + ) +} + +function JobRow({ job }: { job: CostImportJob }) { + const isExport = job.entity === "bulk_product_routing_export" + const progressPct = + job.totalRows > 0 ? Math.round((job.processed / job.totalRows) * 100) : 0 + const isActive = job.status === "PENDING" || job.status === "RUNNING" + + const downloadUrl = job.errorFileUrl + const downloadLabel = isExport ? "Download" : "Error Report" + + function fmtDate(iso: string) { + if (!iso) return "—" + try { + return format(parseISO(iso), "dd MMM yyyy HH:mm") + } catch { + return iso + } + } + + return ( + + + #{job.jobId} + + + + {IMPORT_ENTITY_LABELS[job.entity] ?? job.entity} + + + + + + + + + + {job.totalRows > 0 ? ( + + + + {isActive + ? `${job.processed}/${job.totalRows}` + : isExport + ? `${job.success} produk` + : `${job.success} ok · ${job.failed} gagal · ${job.skipped} skip`} + + + ) : ( + — + )} + + + {fmtDate(job.createdAt)} + + + {fmtDate(job.completedAt)} + + + {downloadUrl ? ( + + + + {downloadLabel} + + + ) : ( + — + )} + + + ) +} + +export function ImportJobsPageClient() { + const [entityFilter, setEntityFilter] = useState(ALL) + const [statusFilter, setStatusFilter] = useState(ALL) + const [page, setPage] = useState(1) + const pageSize = 20 + + const { data, isLoading, refetch, isFetching } = useImportJobs( + { + entity: entityFilter === ALL ? "" : entityFilter, + status: statusFilter === ALL ? "" : statusFilter, + page, + pageSize, + }, + 5000, + ) + + const items = data?.items ?? [] + + return ( + + + void refetch()} + disabled={isFetching} + > + + Refresh + + + + + { setEntityFilter(v); setPage(1) }}> + + + + + {ENTITY_OPTIONS.map((o) => ( + + {o.label} + + ))} + + + + { setStatusFilter(v); setPage(1) }}> + + + + + {STATUS_OPTIONS.map((o) => ( + + {o.label} + + ))} + + + + + + + + + Job ID + Tipe + Status + Progress + Dibuat + Selesai + File + + + + {isLoading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 7 }).map((__, j) => ( + + + + ))} + + )) + ) : items.length === 0 ? ( + + + + + + ) : ( + items.map((job) => ) + )} + + + + + {(data?.totalItems ?? 0) > pageSize && ( + {}} + /> + )} + + ) +} diff --git a/src/app/(dashboard)/finance/import-jobs/loading.tsx b/src/app/(dashboard)/finance/import-jobs/loading.tsx new file mode 100644 index 0000000..1bb802c --- /dev/null +++ b/src/app/(dashboard)/finance/import-jobs/loading.tsx @@ -0,0 +1,13 @@ +import { TableSkeleton } from "@/components/loading" + +export default function ImportJobsLoading() { + return ( + + + + + + + + ) +} diff --git a/src/app/(dashboard)/finance/import-jobs/page.tsx b/src/app/(dashboard)/finance/import-jobs/page.tsx new file mode 100644 index 0000000..d97557a --- /dev/null +++ b/src/app/(dashboard)/finance/import-jobs/page.tsx @@ -0,0 +1,7 @@ +import { ImportJobsPageClient } from "./import-jobs-page-client" + +export const metadata = { title: "Import / Export Jobs" } + +export default function ImportJobsPage() { + return +} diff --git a/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx b/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx index 4ddef85..31f011b 100644 --- a/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx +++ b/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx @@ -1,13 +1,21 @@ "use client" -import { CheckCircle2, Package, PauseCircle, Plus } from "lucide-react" +import { CheckCircle2, ChevronDown, FileSpreadsheet, Package, PauseCircle, Plus, Upload } from "lucide-react" import { useState } from "react" +import { useRouter } from "next/navigation" import { useQueryClient } from "@tanstack/react-query" +import { toast } from "sonner" import { PageHeader } from "@/components/common/page-header" import { KpiCard, KpiGrid } from "@/components/common" import { DebouncedSearchInput } from "@/components/common/debounced-search-input" import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" @@ -17,11 +25,14 @@ import { ProductMasterFormDialog, ProductMasterTable, } from "@/components/finance/cost-product-master" -import { ImportExportToolbar } from "@/components/finance/costing/import-export-toolbar" +import { BulkImportDialog } from "@/components/finance/costing/bulk-import-dialog" +import { ImportDialog } from "@/components/finance/costing/import-dialog" import { ProductTypeCombobox } from "@/components/finance/comboboxes" import { DataTablePagination } from "@/components/shared" import { useCostProductMasterCounts, useCostProductMasters, costProductMasterKeys } from "@/hooks/finance/use-cost-product-master" +import { useExportData } from "@/hooks/finance/use-cost-import" import { useUrlState } from "@/lib/hooks" +import { exportBulkProductRouting } from "@/services/finance/cost-import-api" import type { CostProductMaster, ListCostProductMastersParams } from "@/types/finance/cost-product-master" const defaultFilters: ListCostProductMastersParams = { @@ -37,11 +48,17 @@ export default function ProductMasterPageClient() { const { data, isLoading } = useCostProductMasters(filters) const { data: counts, isLoading: countsLoading } = useCostProductMasterCounts() const queryClient = useQueryClient() + const router = useRouter() const [formOpen, setFormOpen] = useState(false) const [erpOpen, setErpOpen] = useState(false) const [deactivateOpen, setDeactivateOpen] = useState(false) + const [importOpen, setImportOpen] = useState(false) + const [bulkImportOpen, setBulkImportOpen] = useState(false) const [editing, setEditing] = useState(null) + const [bulkExportLoading, setBulkExportLoading] = useState(false) + + const { exportEntity, loading: exportLoading } = useExportData() function openCreate() { setEditing(null) @@ -60,6 +77,29 @@ export default function ProductMasterPageClient() { setDeactivateOpen(true) } + function handleExport() { + void exportEntity("product_master") + } + + async function handleBulkExport() { + setBulkExportLoading(true) + try { + const result = await exportBulkProductRouting() + toast.success(`Export dijadwalkan — Job #${result.jobId}`, { + description: "File akan tersedia di halaman Import Jobs setelah selesai diproses.", + action: { + label: "Lihat Jobs", + onClick: () => router.push("/finance/import-jobs"), + }, + duration: 8000, + }) + } catch (e) { + toast.error(`Export gagal: ${e instanceof Error ? e.message : String(e)}`) + } finally { + setBulkExportLoading(false) + } + } + const items = data?.items ?? [] const pagination = data?.pagination const totalItems = Number(pagination?.totalItems ?? 0) @@ -70,12 +110,44 @@ export default function ProductMasterPageClient() { title="Product Master" subtitle="Costing product identity (CPM_). Codes are auto-generated as CST + type + YYMM + 6-digit sequence." > - - queryClient.invalidateQueries({ queryKey: costProductMasterKeys.all }) - } - /> + {/* Import dropdown */} + + + + + Import + + + + + setImportOpen(true)}> + Import Produk + + setBulkImportOpen(true)}> + Import Produk + Routing (Bulk) + + + + + {/* Export dropdown */} + + + + + Export + + + + + + Export Produk + + void handleBulkExport()}> + Export Produk + Routing + + + + New product @@ -145,6 +217,14 @@ export default function ProductMasterPageClient() { /> )} + + queryClient.invalidateQueries({ queryKey: costProductMasterKeys.all }) + } + /> + ) } diff --git a/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts new file mode 100644 index 0000000..588ee69 --- /dev/null +++ b/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts @@ -0,0 +1,52 @@ +// POST /api/v1/finance/costing/export/bulk_product_routing +// Queue an async export of product master + routing data to MinIO. +// Accepts JSON body: { productTypeCodes?: string[] } +// Returns: { jobId: number, status: string } + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const productTypeCodes: string[] = Array.isArray(body.productTypeCodes) + ? (body.productTypeCodes as string[]) + : [] + + const includeRouting: boolean = + typeof body.includeRouting === "boolean" ? body.includeRouting : true + const activeOnly: boolean = + typeof body.activeOnly === "boolean" ? body.activeOnly : false + + const res = await getCostDataImportClient().exportBulkProductRouting( + { productTypeCodes, includeRouting, activeOnly }, + metadata, + ) + + return NextResponse.json({ + base: res.base, + jobId: res.jobId, + status: res.status, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error exporting bulk product routing:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to export bulk product routing", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/app/api/v1/finance/costing/import-jobs/route.ts b/src/app/api/v1/finance/costing/import-jobs/route.ts new file mode 100644 index 0000000..23208fe --- /dev/null +++ b/src/app/api/v1/finance/costing/import-jobs/route.ts @@ -0,0 +1,48 @@ +// GET /api/v1/finance/costing/import-jobs — list async import/export jobs + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const metadata = createMetadataFromRequest(request) + + const res = await getCostDataImportClient().listCostImportJobs( + { + entity: searchParams.get("entity") ?? "", + status: searchParams.get("status") ?? "", + pagination: { + page: Number(searchParams.get("page")) || 1, + pageSize: Number(searchParams.get("pageSize")) || 20, + }, + }, + metadata, + ) + + return NextResponse.json({ + base: res.base, + data: res.data, + pagination: res.pagination, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error listing import jobs:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to list import jobs", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/app/api/v1/finance/costing/import/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/import/bulk_product_routing/route.ts new file mode 100644 index 0000000..8c66313 --- /dev/null +++ b/src/app/api/v1/finance/costing/import/bulk_product_routing/route.ts @@ -0,0 +1,46 @@ +// POST /api/v1/finance/costing/import/bulk_product_routing +// Import product master + routing data from an Excel file. +// Accepts JSON body: { fileContent: number[], fileName: string, duplicateAction?: string } +// Returns: { base, data: { jobId: number } } + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const fileContent = new Uint8Array(body.fileContent as number[]) + const fileName: string = body.fileName ?? "" + const duplicateAction: string = body.duplicateAction ?? "update" + + const res = await getCostDataImportClient().importBulkProductRouting( + { fileContent, fileName, duplicateAction }, + metadata, + ) + + return NextResponse.json({ + base: res.base, + data: { jobId: res.jobId, status: res.status }, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error importing bulk product routing:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to import bulk product routing", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/app/api/v1/finance/costing/template/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/template/bulk_product_routing/route.ts new file mode 100644 index 0000000..8fb8d21 --- /dev/null +++ b/src/app/api/v1/finance/costing/template/bulk_product_routing/route.ts @@ -0,0 +1,136 @@ +import ExcelJS from "exceljs"; +import { NextResponse } from "next/server"; + +const SHEETS: { name: string; headers: string[]; sample: string[] }[] = [ + { + name: "product_master", + headers: [ + "legacy_oracle_sys_id", + "product_type_code", + "product_name", + "shade_code", + "shade_name", + "grade_code", + "description", + "erp_item_code", + "flex_01", + "flex_03", + "is_active", + ], + sample: [ + "PROD-001", + "FINISH", + "Sample Product Name", + "SH-001", + "Shade Red", + "A", + "Sample description", + "ERP-001", + "", + "", + "true", + ], + }, + { + name: "cpp", + headers: [ + "legacy_oracle_sys_id", + "param_code", + "value_numeric", + "value_text", + "value_flag", + ], + sample: ["PROD-001", "PARAM_CODE", "100.5", "", ""], + }, + { + name: "capp", + headers: [ + "legacy_oracle_sys_id", + "param_code", + "is_required", + "display_order", + ], + sample: ["PROD-001", "PARAM_CODE", "true", "1"], + }, + { + name: "route_head", + headers: ["legacy_oracle_sys_id", "notes"], + sample: ["PROD-001", "Main routing"], + }, + { + name: "route_seq", + headers: [ + "legacy_oracle_sys_id", + "route_level", + "route_seq", + "route_name", + "route_item_code", + "position_x", + "position_y", + "cyl_type_id", + ], + sample: ["PROD-001", "1", "1", "Process 1", "SEQ-001", "0", "0", ""], + }, + { + name: "route_rm", + headers: [ + "legacy_oracle_sys_id", + "route_level", + "route_seq", + "rm_type", + "rm_product_legacy_id", + "rm_item_code", + "rm_group_code", + "rm_name", + "rm_item_code_ref", + "ratio", + "sub_type", + "notes", + ], + sample: [ + "PROD-001", + "1", + "1", + "PRODUCT", + "RM-001", + "", + "", + "RM Name", + "", + "1.0", + "", + "", + ], + }, +]; + +export async function GET() { + const wb = new ExcelJS.Workbook(); + + for (const sheet of SHEETS) { + const ws = wb.addWorksheet(sheet.name); + + // Set columns first — this writes the header row (row 1) automatically + ws.columns = sheet.headers.map((h, i) => ({ + header: h, + key: String(i), + width: Math.max(h.length + 4, 16), + })); + + // Add only the sample data row (row 2) + ws.addRow(sheet.sample); + + // Bold header row + ws.getRow(1).font = { bold: true }; + } + + const buffer = await wb.xlsx.writeBuffer(); + return new NextResponse(buffer, { + headers: { + "Content-Type": + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Content-Disposition": + 'attachment; filename="bulk_product_routing_template.xlsx"', + }, + }); +} diff --git a/src/app/api/v1/finance/costing/validate/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/validate/bulk_product_routing/route.ts new file mode 100644 index 0000000..a52dbd8 --- /dev/null +++ b/src/app/api/v1/finance/costing/validate/bulk_product_routing/route.ts @@ -0,0 +1,55 @@ +// POST /api/v1/finance/costing/validate/bulk_product_routing +// Validate a bulk product routing Excel file before importing. +// Accepts JSON body: { fileContent: number[], fileName: string } +// Returns: { isValid: boolean, sheets: BulkSheetValidationResult[] } + +import { NextRequest, NextResponse } from "next/server" +import { + getCostDataImportClient, + createMetadataFromRequest, + isGrpcError, + handleGrpcError, +} from "@/lib/grpc" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const fileContent = new Uint8Array(body.fileContent as number[]) + const fileName: string = body.fileName ?? "" + + const res = await getCostDataImportClient().validateBulkProductRoutingFile( + { fileContent, fileName }, + metadata, + ) + + return NextResponse.json({ + isValid: res.isValid, + sheets: res.sheets.map((sheet) => ({ + sheetName: sheet.sheetName, + totalRows: sheet.totalRows, + errorCount: sheet.errorCount, + warningCount: sheet.warningCount, + sampleErrors: sheet.sampleErrors.map((e) => ({ + rowNumber: e.rowNumber, + field: e.field, + message: e.message, + })), + })), + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error validating bulk product routing file:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to validate bulk product routing file", + validationErrors: [], + }, + }, + { status: 500 }, + ) + } +} diff --git a/src/components/finance/costing/bulk-import-dialog.tsx b/src/components/finance/costing/bulk-import-dialog.tsx new file mode 100644 index 0000000..7a0ef73 --- /dev/null +++ b/src/components/finance/costing/bulk-import-dialog.tsx @@ -0,0 +1,386 @@ +"use client" + +import React, { useEffect, useRef, useState } from "react" +import { + AlertCircle, + CheckCircle2, + ChevronDown, + ChevronRight, + Download, + FileSpreadsheet, + Loader2, + Upload, +} from "lucide-react" +import { toast } from "sonner" + +import { Dialog } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Label } from "@/components/ui/label" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + ScrollableDialogContent, + ScrollableDialogHeader, + ScrollableDialogBody, + ScrollableDialogFooter, +} from "@/components/common/scrollable-dialog" +import { DialogTitle } from "@/components/ui/dialog" +import { + bulkImportProductMasterRouting, + downloadBulkProductRoutingTemplate, + exportBulkProductRouting, + validateBulkProductRoutingFile, +} from "@/services/finance/cost-import-api" +import type { BulkSheetValidationResult, BulkValidationResult } from "@/types/finance/cost-import" + +export interface BulkImportDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +type Step = "upload" | "validating" | "validated" | "submitting" | "done" + +export function BulkImportDialog({ open, onOpenChange }: BulkImportDialogProps) { + const fileRef = useRef(null) + const [file, setFile] = useState(null) + const [step, setStep] = useState("upload") + const [validation, setValidation] = useState(null) + const [expandedSheets, setExpandedSheets] = useState>(new Set()) + const [jobId, setJobId] = useState(null) + const [templateLoading, setTemplateLoading] = useState(false) + + // Stable reset function — called from handleClose and when open changes + function resetState() { + setFile(null) + setStep("upload") + setValidation(null) + setExpandedSheets(new Set()) + setJobId(null) + if (fileRef.current) fileRef.current.value = "" + } + + // Reset all state when dialog opens + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { if (open) resetState() }, [open]) + + function handleFileChange(e: React.ChangeEvent) { + const selected = e.target.files?.[0] ?? null + setFile(selected) + setValidation(null) + setStep("upload") + } + + async function handleDownloadTemplate() { + setTemplateLoading(true) + try { + await downloadBulkProductRoutingTemplate() + } catch (e) { + toast.error(`Template download failed: ${String(e)}`) + } finally { + setTemplateLoading(false) + } + } + + async function handleValidate() { + if (!file) return + setStep("validating") + try { + const result = await validateBulkProductRoutingFile(file) + setValidation(result) + setStep("validated") + } catch (e) { + toast.error(`Validation failed: ${String(e)}`) + setStep("upload") + } + } + + async function handleImport() { + if (!file) return + setStep("submitting") + try { + const result = await bulkImportProductMasterRouting(file, "update") + setJobId(result.jobId) + setStep("done") + toast.success(`Import queued — Job #${result.jobId}`) + } catch (e) { + toast.error(`Import failed: ${String(e)}`) + setStep("validated") + } + } + + function handleClose() { + resetState() + onOpenChange(false) + } + + function toggleSheet(sheetName: string) { + setExpandedSheets((prev) => { + const next = new Set(prev) + if (next.has(sheetName)) { + next.delete(sheetName) + } else { + next.add(sheetName) + } + return next + }) + } + + const hasErrors = validation?.sheets.some((s) => s.errorCount > 0) ?? false + const isValidating = step === "validating" + const isSubmitting = step === "submitting" + const isDone = step === "done" + const canImport = step === "validated" && !hasErrors && (validation?.isValid ?? false) + + return ( + + + + Bulk Import — Product Master & Routing + + + + {/* Template Download — hidden once job submitted */} + {!isDone && ( + + + + + Import Template + + Download template Excel dengan 6 sheet yang diperlukan + + + + + {templateLoading ? ( + + ) : ( + + )} + Download + + + )} + + {/* File Upload — hidden once job submitted */} + {!isDone && ( + + Pilih File + fileRef.current?.click()} + > + + {file ? ( + + + {file.name} + + ({(file.size / 1024).toFixed(1)} KB) + + + ) : ( + <> + + + Klik untuk memilih atau drag & drop file Excel + + + Format: .xlsx + + > + )} + + + )} + + {/* Validating spinner */} + {isValidating && ( + + + Memvalidasi file… + + )} + + {/* Validation results table */} + {validation && !isValidating && ( + + + {validation.isValid ? ( + + ) : ( + + )} + + {validation.isValid + ? "File valid — siap diimport" + : "Validasi gagal — perbaiki error sebelum import"} + + + + + + + + Sheet + Row + Error + Warning + + + + + {validation.sheets.map((sheet) => ( + + 0 ? "bg-destructive/5" : undefined} + > + {sheet.sheetName} + {sheet.totalRows} + + {sheet.errorCount > 0 ? ( + + {sheet.errorCount} + + ) : ( + 0 + )} + + + {sheet.warningCount > 0 ? ( + + {sheet.warningCount} + + ) : ( + 0 + )} + + + {sheet.sampleErrors.length > 0 && ( + toggleSheet(sheet.sheetName)} + > + {expandedSheets.has(sheet.sheetName) ? ( + + ) : ( + + )} + + )} + + + + {/* Sample errors */} + {expandedSheets.has(sheet.sheetName) && + sheet.sampleErrors.map((err, i) => ( + + + Row {err.rowNumber}{err.field ? ` [${err.field}]` : ""}: {err.message} + + + ))} + + ))} + + + + + )} + + {/* Done */} + {isDone && jobId !== null && ( + + + Import dijadwalkan sebagai Job #{jobId}. Notifikasi akan dikirim saat selesai. + + )} + + + + + {isDone ? "Tutup" : "Batal"} + + + {file && step === "upload" && ( + + + Validasi + + )} + + {canImport && ( + + {isSubmitting ? ( + + ) : ( + + )} + {isSubmitting ? "Mengantri…" : "Mulai Import"} + + )} + + + + ) +} + +/** + * Standalone "Export All" button that queues an async bulk export job. + */ +export function BulkExportButton({ + productTypeCodes, +}: { + productTypeCodes?: string[] +}) { + const [loading, setLoading] = useState(false) + + async function handleExport() { + setLoading(true) + try { + const result = await exportBulkProductRouting({ productTypeCodes }) + toast.success(`Export queued — Job #${result.jobId}`) + } catch (e) { + toast.error(`Export failed: ${String(e)}`) + } finally { + setLoading(false) + } + } + + return ( + + {loading ? ( + + ) : ( + + )} + {loading ? "Exporting…" : "Export All"} + + ) +} + +// Re-export BulkSheetValidationResult for any consumers that import from this module +export type { BulkSheetValidationResult, BulkValidationResult } diff --git a/src/hooks/finance/use-cost-import.ts b/src/hooks/finance/use-cost-import.ts index b0fed16..d3ce8fc 100644 --- a/src/hooks/finance/use-cost-import.ts +++ b/src/hooks/finance/use-cost-import.ts @@ -10,15 +10,28 @@ import { importSync, importAsync, getImportJob, + listImportJobs, } from "@/services/finance/cost-import-api" import type { CostImportJob, ImportEntity, SyncImportResult, } from "@/types/finance/cost-import" +import type { ListImportJobsParams } from "@/services/finance/cost-import-api" export const costImportKeys = { job: (id: number) => ["finance", "cost-import", "job", id] as const, + jobs: (params?: ListImportJobsParams) => + ["finance", "cost-import", "jobs", params] as const, +} + +export function useImportJobs(params?: ListImportJobsParams, refetchIntervalMs?: number) { + return useQuery({ + queryKey: costImportKeys.jobs(params), + queryFn: () => listImportJobs(params), + staleTime: 5000, + refetchInterval: refetchIntervalMs, + }) } export function useDownloadTemplate() { diff --git a/src/services/finance/cost-import-api.ts b/src/services/finance/cost-import-api.ts index 76f9c28..7472e96 100644 --- a/src/services/finance/cost-import-api.ts +++ b/src/services/finance/cost-import-api.ts @@ -5,6 +5,9 @@ import type { SyncImportResult, AsyncImportResponse, ImportEntity, + BulkValidationResult, + BulkSheetValidationResult, + BulkRowError, } from "@/types/finance/cost-import" const BASE = "/api/v1/finance/costing" @@ -102,3 +105,162 @@ export async function getImportJob(jobId: number): Promise { const json = await res.json() return json.data as CostImportJob } + +export interface ListImportJobsParams { + entity?: string + status?: string + page?: number + pageSize?: number +} + +export interface ListImportJobsResult { + items: CostImportJob[] + totalItems: number + totalPages: number + currentPage: number + pageSize: number +} + +/** + * List import/export jobs with optional entity/status filters. + */ +export async function listImportJobs( + params?: ListImportJobsParams, +): Promise { + const qs = new URLSearchParams() + if (params?.entity) qs.set("entity", params.entity) + if (params?.status) qs.set("status", params.status) + if (params?.page) qs.set("page", String(params.page)) + if (params?.pageSize) qs.set("pageSize", String(params.pageSize)) + const res = await fetch(`${BASE}/import-jobs?${qs.toString()}`) + if (!res.ok) throw new Error(`List jobs failed: ${res.status}`) + const json = await res.json() + const raw: unknown[] = Array.isArray(json.data) ? json.data : [] + return { + items: raw as CostImportJob[], + totalItems: Number(json.pagination?.totalItems ?? raw.length), + totalPages: Number(json.pagination?.totalPages ?? 1), + currentPage: Number(json.pagination?.currentPage ?? 1), + pageSize: Number(json.pagination?.pageSize ?? 20), + } +} + +// ── Bulk Product Routing ──────────────────────────────────────────────────── + +/** + * Queue a bulk import of product master + routing data from an Excel file. + * Returns a job ID that can be polled with getImportJob(). + */ +export async function bulkImportProductMasterRouting( + file: File, + duplicateAction?: string, +): Promise<{ jobId: number; status: string }> { + const fileContent = Array.from(new Uint8Array(await file.arrayBuffer())) + const res = await fetch(`${BASE}/import/bulk_product_routing`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fileContent, + fileName: file.name, + duplicateAction: duplicateAction ?? "update", + }), + }) + if (!res.ok) throw new Error(`Bulk import failed: ${res.status}`) + const json = await res.json() + return { + jobId: json.data?.jobId ?? json.data?.job_id ?? 0, + status: json.data?.status ?? "", + } +} + +/** + * Validate a bulk product routing Excel file without importing. + * Returns a per-sheet summary with error/warning counts and sample errors. + */ +export async function validateBulkProductRoutingFile( + file: File, +): Promise { + const fileContent = Array.from(new Uint8Array(await file.arrayBuffer())) + const res = await fetch(`${BASE}/validate/bulk_product_routing`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileContent, fileName: file.name }), + }) + if (!res.ok) throw new Error(`Validation failed: ${res.status}`) + const json = await res.json() + + if (!json.base?.isSuccess && json.base?.isSuccess !== undefined) { + throw new Error(json.base?.message || "Validation request failed") + } + + // Normalise: handle both camelCase (gRPC-gateway) and snake_case (raw proto) + const isValid: boolean = json.isValid ?? json.is_valid ?? false + const rawSheets: unknown[] = Array.isArray(json.sheets) ? json.sheets : [] + + const sheets: BulkSheetValidationResult[] = rawSheets.map((s) => { + const sheet = s as Record + const rawErrors: unknown[] = Array.isArray(sheet.sampleErrors) + ? (sheet.sampleErrors as unknown[]) + : Array.isArray(sheet.sample_errors) + ? (sheet.sample_errors as unknown[]) + : [] + + const sampleErrors: BulkRowError[] = rawErrors.map((e) => { + const err = e as Record + return { + rowNumber: Number(err.rowNumber ?? err.row_number ?? 0), + field: String(err.field ?? ""), + message: String(err.message ?? ""), + } + }) + + return { + sheetName: String(sheet.sheetName ?? sheet.sheet_name ?? ""), + totalRows: Number(sheet.totalRows ?? sheet.total_rows ?? 0), + errorCount: Number(sheet.errorCount ?? sheet.error_count ?? 0), + warningCount: Number(sheet.warningCount ?? sheet.warning_count ?? 0), + sampleErrors, + } + }) + + return { isValid, sheets } +} + +/** + * Queue an async export of all product master + routing data to MinIO. + * Returns a job ID that can be polled or viewed on the import-jobs page. + */ +export async function exportBulkProductRouting(options?: { + productTypeCodes?: string[] +}): Promise<{ jobId: number; status: string }> { + const res = await fetch(`${BASE}/export/bulk_product_routing`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ productTypeCodes: options?.productTypeCodes ?? [] }), + }) + if (!res.ok) throw new Error(`Bulk export failed: ${res.status}`) + const json = await res.json() + if (json.base?.isSuccess === false) { + throw new Error(json.base?.message || "Bulk export failed") + } + return { + jobId: json.jobId ?? json.job_id ?? json.data?.jobId ?? json.data?.job_id ?? 0, + status: json.status ?? json.data?.status ?? "", + } +} + +/** + * Download the blank bulk product routing import template. + * Triggers a browser file download directly. + */ +export async function downloadBulkProductRoutingTemplate(): Promise { + const res = await fetch("/api/v1/finance/costing/template/bulk_product_routing") + if (!res.ok) throw new Error("Failed to download template") + const blob = await res.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = "bulk_product_routing_template.xlsx" + a.click() + URL.revokeObjectURL(url) +} diff --git a/src/types/finance/cost-import.ts b/src/types/finance/cost-import.ts index 7155f70..38b92f5 100644 --- a/src/types/finance/cost-import.ts +++ b/src/types/finance/cost-import.ts @@ -8,6 +8,8 @@ export type ImportEntity = | "product_master" | "capp" | "cpp" + | "bulk_product_routing" + | "bulk_product_routing_export" export interface CostImportJob { jobId: number @@ -77,3 +79,46 @@ export function normalizeCostImportJob(raw: RawCostImportJob): CostImportJob { completedAt: raw.completedAt ?? raw.completed_at ?? "", } } + +// ── Import entity label map ───────────────────────────────────────────────── + +/** Human-readable labels for all import/export entity types. */ +export const IMPORT_ENTITY_LABELS: Record = { + product_type: "Product Type", + parameter: "Parameter", + product_master: "Product Master", + capp: "Cost Applicable Parameters", + cpp: "Cost Product Parameters", + bulk_product_routing: "Bulk Import (Product Master + Routing)", + bulk_product_routing_export: "Bulk Export (Product Master + Routing)", +} + +// ── Bulk Product Routing import/export ───────────────────────────────────── + +/** Entity constant for the bulk import (product master + routing). */ +export const ENTITY_BULK_PRODUCT_ROUTING = "bulk_product_routing" + +/** Entity constant for the bulk export job (product master + routing). */ +export const ENTITY_BULK_PRODUCT_ROUTING_EXPORT = "bulk_product_routing_export" + +/** Row-level error from a single sheet during bulk validation. */ +export interface BulkRowError { + rowNumber: number + field: string + message: string +} + +/** Per-sheet result from ValidateBulkProductRoutingFile. */ +export interface BulkSheetValidationResult { + sheetName: string + totalRows: number + errorCount: number + warningCount: number + sampleErrors: BulkRowError[] +} + +/** Full result from ValidateBulkProductRoutingFile. */ +export interface BulkValidationResult { + isValid: boolean + sheets: BulkSheetValidationResult[] +} diff --git a/src/types/generated/finance/v1/cost_import.ts b/src/types/generated/finance/v1/cost_import.ts index 8180c78..53c571b 100644 --- a/src/types/generated/finance/v1/cost_import.ts +++ b/src/types/generated/finance/v1/cost_import.ts @@ -107,6 +107,55 @@ export interface DownloadCostProductParameterTemplateResponse { fileName: string; } +export interface ImportRowError { + rowNumber: number; + field: string; + message: string; +} + +export interface BulkSheetValidationResult { + sheetName: string; + totalRows: number; + errorCount: number; + warningCount: number; + sampleErrors: ImportRowError[]; +} + +export interface ImportBulkProductRoutingRequest { + fileContent: Uint8Array; + fileName: string; + duplicateAction: string; +} + +export interface ImportBulkProductRoutingResponse { + base: BaseResponse | undefined; + jobId: number; + status: string; +} + +export interface ValidateBulkProductRoutingFileRequest { + fileContent: Uint8Array; + fileName: string; +} + +export interface ValidateBulkProductRoutingFileResponse { + base: BaseResponse | undefined; + isValid: boolean; + sheets: BulkSheetValidationResult[]; +} + +export interface ExportBulkProductRoutingRequest { + productTypeCodes: string[]; + includeRouting: boolean; + activeOnly: boolean; +} + +export interface ExportBulkProductRoutingResponse { + base: BaseResponse | undefined; + jobId: number; + status: string; +} + function createBaseCostImportJob(): CostImportJob { return { jobId: 0, @@ -1743,72 +1792,924 @@ export const DownloadCostProductParameterTemplateResponse: MessageFns = { + encode(message: ImportRowError, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.rowNumber !== 0) { + writer.uint32(8).int32(message.rowNumber); + } + if (message.field !== "") { + writer.uint32(18).string(message.field); + } + if (message.message !== "") { + writer.uint32(26).string(message.message); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ImportRowError { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseImportRowError(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.rowNumber = reader.int32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.field = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.message = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ImportRowError { + return { + rowNumber: isSet(object.rowNumber) + ? globalThis.Number(object.rowNumber) + : isSet(object.row_number) + ? globalThis.Number(object.row_number) + : 0, + field: isSet(object.field) ? globalThis.String(object.field) : "", + message: isSet(object.message) ? globalThis.String(object.message) : "", + }; + }, + + toJSON(message: ImportRowError): unknown { + const obj: any = {}; + if (message.rowNumber !== 0) { + obj.rowNumber = Math.round(message.rowNumber); + } + if (message.field !== "") { + obj.field = message.field; + } + if (message.message !== "") { + obj.message = message.message; + } + return obj; + }, + + create(base?: DeepPartial): ImportRowError { + return ImportRowError.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ImportRowError { + const message = createBaseImportRowError(); + message.rowNumber = object.rowNumber ?? 0; + message.field = object.field ?? ""; + message.message = object.message ?? ""; + return message; + }, +}; + +function createBaseBulkSheetValidationResult(): BulkSheetValidationResult { + return { sheetName: "", totalRows: 0, errorCount: 0, warningCount: 0, sampleErrors: [] }; +} + +export const BulkSheetValidationResult: MessageFns = { + encode(message: BulkSheetValidationResult, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.sheetName !== "") { + writer.uint32(10).string(message.sheetName); + } + if (message.totalRows !== 0) { + writer.uint32(16).int32(message.totalRows); + } + if (message.errorCount !== 0) { + writer.uint32(24).int32(message.errorCount); + } + if (message.warningCount !== 0) { + writer.uint32(32).int32(message.warningCount); + } + for (const v of message.sampleErrors) { + ImportRowError.encode(v!, writer.uint32(42).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): BulkSheetValidationResult { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseBulkSheetValidationResult(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.sheetName = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.totalRows = reader.int32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.errorCount = reader.int32(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.warningCount = reader.int32(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.sampleErrors.push(ImportRowError.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): BulkSheetValidationResult { + return { + sheetName: isSet(object.sheetName) + ? globalThis.String(object.sheetName) + : isSet(object.sheet_name) + ? globalThis.String(object.sheet_name) + : "", + totalRows: isSet(object.totalRows) + ? globalThis.Number(object.totalRows) + : isSet(object.total_rows) + ? globalThis.Number(object.total_rows) + : 0, + errorCount: isSet(object.errorCount) + ? globalThis.Number(object.errorCount) + : isSet(object.error_count) + ? globalThis.Number(object.error_count) + : 0, + warningCount: isSet(object.warningCount) + ? globalThis.Number(object.warningCount) + : isSet(object.warning_count) + ? globalThis.Number(object.warning_count) + : 0, + sampleErrors: globalThis.Array.isArray(object?.sampleErrors) + ? object.sampleErrors.map((e: any) => ImportRowError.fromJSON(e)) + : globalThis.Array.isArray(object?.sample_errors) + ? object.sample_errors.map((e: any) => ImportRowError.fromJSON(e)) + : [], + }; + }, + + toJSON(message: BulkSheetValidationResult): unknown { + const obj: any = {}; + if (message.sheetName !== "") { + obj.sheetName = message.sheetName; + } + if (message.totalRows !== 0) { + obj.totalRows = Math.round(message.totalRows); + } + if (message.errorCount !== 0) { + obj.errorCount = Math.round(message.errorCount); + } + if (message.warningCount !== 0) { + obj.warningCount = Math.round(message.warningCount); + } + if (message.sampleErrors?.length) { + obj.sampleErrors = message.sampleErrors.map((e) => ImportRowError.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): BulkSheetValidationResult { + return BulkSheetValidationResult.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): BulkSheetValidationResult { + const message = createBaseBulkSheetValidationResult(); + message.sheetName = object.sheetName ?? ""; + message.totalRows = object.totalRows ?? 0; + message.errorCount = object.errorCount ?? 0; + message.warningCount = object.warningCount ?? 0; + message.sampleErrors = object.sampleErrors?.map((e) => ImportRowError.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseImportBulkProductRoutingRequest(): ImportBulkProductRoutingRequest { + return { fileContent: new Uint8Array(0), fileName: "", duplicateAction: "" }; +} + +export const ImportBulkProductRoutingRequest: MessageFns = { + encode(message: ImportBulkProductRoutingRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.fileContent.length !== 0) { + writer.uint32(10).bytes(message.fileContent); + } + if (message.fileName !== "") { + writer.uint32(18).string(message.fileName); + } + if (message.duplicateAction !== "") { + writer.uint32(26).string(message.duplicateAction); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ImportBulkProductRoutingRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseImportBulkProductRoutingRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.fileContent = reader.bytes(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.fileName = reader.string(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.duplicateAction = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ImportBulkProductRoutingRequest { + return { + fileContent: isSet(object.fileContent) + ? bytesFromBase64(object.fileContent) + : isSet(object.file_content) + ? bytesFromBase64(object.file_content) + : new Uint8Array(0), + fileName: isSet(object.fileName) + ? globalThis.String(object.fileName) + : isSet(object.file_name) + ? globalThis.String(object.file_name) + : "", + duplicateAction: isSet(object.duplicateAction) + ? globalThis.String(object.duplicateAction) + : isSet(object.duplicate_action) + ? globalThis.String(object.duplicate_action) + : "", + }; + }, + + toJSON(message: ImportBulkProductRoutingRequest): unknown { + const obj: any = {}; + if (message.fileContent.length !== 0) { + obj.fileContent = base64FromBytes(message.fileContent); + } + if (message.fileName !== "") { + obj.fileName = message.fileName; + } + if (message.duplicateAction !== "") { + obj.duplicateAction = message.duplicateAction; + } + return obj; + }, + + create(base?: DeepPartial): ImportBulkProductRoutingRequest { + return ImportBulkProductRoutingRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ImportBulkProductRoutingRequest { + const message = createBaseImportBulkProductRoutingRequest(); + message.fileContent = object.fileContent ?? new Uint8Array(0); + message.fileName = object.fileName ?? ""; + message.duplicateAction = object.duplicateAction ?? ""; + return message; + }, +}; + +function createBaseImportBulkProductRoutingResponse(): ImportBulkProductRoutingResponse { + return { base: undefined, jobId: 0, status: "" }; +} + +export const ImportBulkProductRoutingResponse: MessageFns = { + encode(message: ImportBulkProductRoutingResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.base !== undefined) { + BaseResponse.encode(message.base, writer.uint32(10).fork()).join(); + } + if (message.jobId !== 0) { + writer.uint32(16).int64(message.jobId); + } + if (message.status !== "") { + writer.uint32(26).string(message.status); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ImportBulkProductRoutingResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseImportBulkProductRoutingResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.base = BaseResponse.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.jobId = longToNumber(reader.int64()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.status = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ImportBulkProductRoutingResponse { + return { + base: isSet(object.base) ? BaseResponse.fromJSON(object.base) : undefined, + jobId: isSet(object.jobId) + ? globalThis.Number(object.jobId) + : isSet(object.job_id) + ? globalThis.Number(object.job_id) + : 0, + status: isSet(object.status) ? globalThis.String(object.status) : "", + }; + }, + + toJSON(message: ImportBulkProductRoutingResponse): unknown { + const obj: any = {}; + if (message.base !== undefined) { + obj.base = BaseResponse.toJSON(message.base); + } + if (message.jobId !== 0) { + obj.jobId = Math.round(message.jobId); + } + if (message.status !== "") { + obj.status = message.status; + } + return obj; + }, + + create(base?: DeepPartial): ImportBulkProductRoutingResponse { + return ImportBulkProductRoutingResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ImportBulkProductRoutingResponse { + const message = createBaseImportBulkProductRoutingResponse(); + message.base = (object.base !== undefined && object.base !== null) + ? BaseResponse.fromPartial(object.base) + : undefined; + message.jobId = object.jobId ?? 0; + message.status = object.status ?? ""; + return message; + }, +}; + +function createBaseValidateBulkProductRoutingFileRequest(): ValidateBulkProductRoutingFileRequest { + return { fileContent: new Uint8Array(0), fileName: "" }; +} + +export const ValidateBulkProductRoutingFileRequest: MessageFns = { + encode(message: ValidateBulkProductRoutingFileRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.fileContent.length !== 0) { + writer.uint32(10).bytes(message.fileContent); + } + if (message.fileName !== "") { + writer.uint32(18).string(message.fileName); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ValidateBulkProductRoutingFileRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseValidateBulkProductRoutingFileRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.fileContent = reader.bytes(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.fileName = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ValidateBulkProductRoutingFileRequest { + return { + fileContent: isSet(object.fileContent) + ? bytesFromBase64(object.fileContent) + : isSet(object.file_content) + ? bytesFromBase64(object.file_content) + : new Uint8Array(0), + fileName: isSet(object.fileName) + ? globalThis.String(object.fileName) + : isSet(object.file_name) + ? globalThis.String(object.file_name) + : "", + }; + }, + + toJSON(message: ValidateBulkProductRoutingFileRequest): unknown { + const obj: any = {}; + if (message.fileContent.length !== 0) { + obj.fileContent = base64FromBytes(message.fileContent); + } + if (message.fileName !== "") { + obj.fileName = message.fileName; + } + return obj; + }, + + create(base?: DeepPartial): ValidateBulkProductRoutingFileRequest { + return ValidateBulkProductRoutingFileRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ValidateBulkProductRoutingFileRequest { + const message = createBaseValidateBulkProductRoutingFileRequest(); + message.fileContent = object.fileContent ?? new Uint8Array(0); + message.fileName = object.fileName ?? ""; + return message; + }, +}; + +function createBaseValidateBulkProductRoutingFileResponse(): ValidateBulkProductRoutingFileResponse { + return { base: undefined, isValid: false, sheets: [] }; +} + +export const ValidateBulkProductRoutingFileResponse: MessageFns = { + encode(message: ValidateBulkProductRoutingFileResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.base !== undefined) { + BaseResponse.encode(message.base, writer.uint32(10).fork()).join(); + } + if (message.isValid !== false) { + writer.uint32(16).bool(message.isValid); + } + for (const v of message.sheets) { + BulkSheetValidationResult.encode(v!, writer.uint32(26).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ValidateBulkProductRoutingFileResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseValidateBulkProductRoutingFileResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.base = BaseResponse.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.isValid = reader.bool(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.sheets.push(BulkSheetValidationResult.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ValidateBulkProductRoutingFileResponse { + return { + base: isSet(object.base) ? BaseResponse.fromJSON(object.base) : undefined, + isValid: isSet(object.isValid) + ? globalThis.Boolean(object.isValid) + : isSet(object.is_valid) + ? globalThis.Boolean(object.is_valid) + : false, + sheets: globalThis.Array.isArray(object?.sheets) + ? object.sheets.map((e: any) => BulkSheetValidationResult.fromJSON(e)) + : [], + }; + }, + + toJSON(message: ValidateBulkProductRoutingFileResponse): unknown { + const obj: any = {}; + if (message.base !== undefined) { + obj.base = BaseResponse.toJSON(message.base); + } + if (message.isValid !== false) { + obj.isValid = message.isValid; + } + if (message.sheets?.length) { + obj.sheets = message.sheets.map((e) => BulkSheetValidationResult.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): ValidateBulkProductRoutingFileResponse { + return ValidateBulkProductRoutingFileResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ValidateBulkProductRoutingFileResponse { + const message = createBaseValidateBulkProductRoutingFileResponse(); + message.base = (object.base !== undefined && object.base !== null) + ? BaseResponse.fromPartial(object.base) + : undefined; + message.isValid = object.isValid ?? false; + message.sheets = object.sheets?.map((e) => BulkSheetValidationResult.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseExportBulkProductRoutingRequest(): ExportBulkProductRoutingRequest { + return { productTypeCodes: [], includeRouting: false, activeOnly: false }; +} + +export const ExportBulkProductRoutingRequest: MessageFns = { + encode(message: ExportBulkProductRoutingRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.productTypeCodes) { + writer.uint32(10).string(v!); + } + if (message.includeRouting !== false) { + writer.uint32(16).bool(message.includeRouting); + } + if (message.activeOnly !== false) { + writer.uint32(24).bool(message.activeOnly); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ExportBulkProductRoutingRequest { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseExportBulkProductRoutingRequest(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.productTypeCodes.push(reader.string()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.includeRouting = reader.bool(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.activeOnly = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ExportBulkProductRoutingRequest { + return { + productTypeCodes: globalThis.Array.isArray(object?.productTypeCodes) + ? object.productTypeCodes.map((e: any) => globalThis.String(e)) + : globalThis.Array.isArray(object?.product_type_codes) + ? object.product_type_codes.map((e: any) => globalThis.String(e)) + : [], + includeRouting: isSet(object.includeRouting) + ? globalThis.Boolean(object.includeRouting) + : isSet(object.include_routing) + ? globalThis.Boolean(object.include_routing) + : false, + activeOnly: isSet(object.activeOnly) + ? globalThis.Boolean(object.activeOnly) + : isSet(object.active_only) + ? globalThis.Boolean(object.active_only) + : false, + }; + }, + + toJSON(message: ExportBulkProductRoutingRequest): unknown { + const obj: any = {}; + if (message.productTypeCodes?.length) { + obj.productTypeCodes = message.productTypeCodes; + } + if (message.includeRouting !== false) { + obj.includeRouting = message.includeRouting; + } + if (message.activeOnly !== false) { + obj.activeOnly = message.activeOnly; + } + return obj; + }, + + create(base?: DeepPartial): ExportBulkProductRoutingRequest { + return ExportBulkProductRoutingRequest.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ExportBulkProductRoutingRequest { + const message = createBaseExportBulkProductRoutingRequest(); + message.productTypeCodes = object.productTypeCodes?.map((e) => e) || []; + message.includeRouting = object.includeRouting ?? false; + message.activeOnly = object.activeOnly ?? false; + return message; + }, +}; + +function createBaseExportBulkProductRoutingResponse(): ExportBulkProductRoutingResponse { + return { base: undefined, jobId: 0, status: "" }; +} + +export const ExportBulkProductRoutingResponse: MessageFns = { + encode(message: ExportBulkProductRoutingResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.base !== undefined) { + BaseResponse.encode(message.base, writer.uint32(10).fork()).join(); + } + if (message.jobId !== 0) { + writer.uint32(16).int64(message.jobId); + } + if (message.status !== "") { + writer.uint32(26).string(message.status); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ExportBulkProductRoutingResponse { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseExportBulkProductRoutingResponse(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.base = BaseResponse.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.jobId = longToNumber(reader.int64()); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.status = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): ExportBulkProductRoutingResponse { + return { + base: isSet(object.base) ? BaseResponse.fromJSON(object.base) : undefined, + jobId: isSet(object.jobId) + ? globalThis.Number(object.jobId) + : isSet(object.job_id) + ? globalThis.Number(object.job_id) + : 0, + status: isSet(object.status) ? globalThis.String(object.status) : "", + }; + }, + + toJSON(message: ExportBulkProductRoutingResponse): unknown { + const obj: any = {}; + if (message.base !== undefined) { + obj.base = BaseResponse.toJSON(message.base); + } + if (message.jobId !== 0) { + obj.jobId = Math.round(message.jobId); + } + if (message.status !== "") { + obj.status = message.status; + } + return obj; + }, + + create(base?: DeepPartial): ExportBulkProductRoutingResponse { + return ExportBulkProductRoutingResponse.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): ExportBulkProductRoutingResponse { + const message = createBaseExportBulkProductRoutingResponse(); + message.base = (object.base !== undefined && object.base !== null) + ? BaseResponse.fromPartial(object.base) + : undefined; + message.jobId = object.jobId ?? 0; + message.status = object.status ?? ""; + return message; + }, +}; + +export type CostDataImportServiceDefinition = typeof CostDataImportServiceDefinition; +export const CostDataImportServiceDefinition = { + name: "CostDataImportService", + fullName: "finance.v1.CostDataImportService", + methods: { + getCostImportJob: { + name: "GetCostImportJob", + requestType: GetCostImportJobRequest, + requestStream: false, + responseType: GetCostImportJobResponse, + responseStream: false, + options: {}, + }, + listCostImportJobs: { + name: "ListCostImportJobs", + requestType: ListCostImportJobsRequest, + requestStream: false, + responseType: ListCostImportJobsResponse, + responseStream: false, + options: {}, + }, + importCostApplicableParams: { + name: "ImportCostApplicableParams", + requestType: ImportCostApplicableParamsRequest, + requestStream: false, + responseType: ImportCostApplicableParamsResponse, + responseStream: false, + options: {}, + }, + exportCostApplicableParams: { + name: "ExportCostApplicableParams", + requestType: ExportCostApplicableParamsRequest, + requestStream: false, + responseType: ExportCostApplicableParamsResponse, + responseStream: false, + options: {}, + }, + downloadCostApplicableParamTemplate: { + name: "DownloadCostApplicableParamTemplate", + requestType: DownloadCostApplicableParamTemplateRequest, + requestStream: false, + responseType: DownloadCostApplicableParamTemplateResponse, + responseStream: false, + options: {}, + }, + importCostProductParameters: { + name: "ImportCostProductParameters", + requestType: ImportCostProductParametersRequest, + requestStream: false, + responseType: ImportCostProductParametersResponse, + responseStream: false, + options: {}, + }, + exportCostProductParameters: { + name: "ExportCostProductParameters", + requestType: ExportCostProductParametersRequest, + requestStream: false, + responseType: ExportCostProductParametersResponse, + responseStream: false, + options: {}, + }, + downloadCostProductParameterTemplate: { + name: "DownloadCostProductParameterTemplate", + requestType: DownloadCostProductParameterTemplateRequest, + requestStream: false, + responseType: DownloadCostProductParameterTemplateResponse, + responseStream: false, + options: {}, + }, + importBulkProductRouting: { + name: "ImportBulkProductRouting", + requestType: ImportBulkProductRoutingRequest, + requestStream: false, + responseType: ImportBulkProductRoutingResponse, + responseStream: false, + options: {}, + }, + validateBulkProductRoutingFile: { + name: "ValidateBulkProductRoutingFile", + requestType: ValidateBulkProductRoutingFileRequest, + requestStream: false, + responseType: ValidateBulkProductRoutingFileResponse, + responseStream: false, + options: {}, + }, + exportBulkProductRouting: { + name: "ExportBulkProductRouting", + requestType: ExportBulkProductRoutingRequest, + requestStream: false, + responseType: ExportBulkProductRoutingResponse, responseStream: false, options: {}, },
+ {isActive + ? `${job.processed}/${job.totalRows}` + : isExport + ? `${job.success} produk` + : `${job.success} ok · ${job.failed} gagal · ${job.skipped} skip`} +
Import Template
+ Download template Excel dengan 6 sheet yang diperlukan +
+ Klik untuk memilih atau drag & drop file Excel +
+ Format: .xlsx +