diff --git a/package-lock.json b/package-lock.json index bcb1605..d41a1fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", + "exceljs": "^4.4.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -1354,6 +1355,47 @@ "node": ">=18" } }, + "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/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -3715,11 +3757,105 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/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/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/balanced-match": { "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/base64id": { @@ -3765,6 +3901,28 @@ "node": ">=14.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3778,6 +3936,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", @@ -3849,7 +4024,6 @@ "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", @@ -3904,6 +4078,39 @@ "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-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3911,6 +4118,23 @@ "dev": true, "license": "MIT" }, + "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/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4040,6 +4264,18 @@ ], "license": "CC-BY-4.0" }, + "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": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -4214,11 +4450,25 @@ "dev": true, "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/connect": { @@ -4357,6 +4607,12 @@ "node": ">=6.6.0" } }, + "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/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4371,6 +4627,31 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4433,6 +4714,12 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4577,6 +4864,45 @@ "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/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/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4633,6 +4959,15 @@ "node": ">=0.10.0" } }, + "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/engine.io": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", @@ -4920,6 +5255,26 @@ "node": ">=18.0.0" } }, + "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/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -4998,6 +5353,19 @@ "dev": true, "license": "MIT" }, + "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", @@ -5119,6 +5487,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -5151,7 +5525,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -5169,6 +5542,35 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/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/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5256,7 +5658,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5310,7 +5711,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/has-flag": { @@ -5527,6 +5927,26 @@ "url": "https://opencollective.com/express" } }, + "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-walk": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", @@ -5556,6 +5976,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/immutable": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", @@ -5578,7 +6004,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -5589,7 +6014,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -5749,6 +6173,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", @@ -5952,6 +6382,48 @@ ], "license": "MIT" }, + "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/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/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -6404,6 +6876,63 @@ "node": ">=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/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/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/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/listr2": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", @@ -6483,6 +7012,85 @@ "dev": true, "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.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/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -6789,7 +7397,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6802,7 +7409,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" @@ -6945,7 +7551,6 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -7156,7 +7761,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7337,7 +7941,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -7446,6 +8049,12 @@ "node": "^20.17.0 || >=22.9.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/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -7527,7 +8136,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7677,6 +8285,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "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/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -7764,6 +8378,50 @@ "node": ">= 0.10" } }, + "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.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "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/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -7953,6 +8611,26 @@ "tslib": "^2.1.0" } }, + "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-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -8000,6 +8678,18 @@ "@parcel/watcher": "^2.4.1" } }, + "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/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -8052,6 +8742,12 @@ "node": ">= 18" } }, + "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/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -8520,6 +9216,15 @@ "node": ">=8.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -8597,6 +9302,22 @@ "node": ">=18" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -8628,7 +9349,6 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -8657,6 +9377,15 @@ "node": ">=0.6" } }, + "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/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -8789,6 +9518,54 @@ "node": ">= 0.8" } }, + "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/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -8820,6 +9597,12 @@ "browserslist": ">= 4.21.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/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8830,6 +9613,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -9092,7 +9884,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -9117,6 +9908,12 @@ } } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -9175,6 +9972,41 @@ "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/zod": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", diff --git a/package.json b/package.json index 46bb015..b7e9f3b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "bootstrap": "^5.3.8", "bootstrap-icons": "^1.13.1", "chart.js": "^4.5.1", + "exceljs": "^4.4.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html index 513d6e6..5f3da29 100644 --- a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.html @@ -35,6 +35,14 @@
+ + @@ -88,7 +92,6 @@ Itens por pág:
-
diff --git a/src/app/pages/dados-usuarios/dados-usuarios.ts b/src/app/pages/dados-usuarios/dados-usuarios.ts index b94a1c9..3526def 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.ts +++ b/src/app/pages/dados-usuarios/dados-usuarios.ts @@ -2,7 +2,9 @@ import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { TableExportService } from '../../services/table-export.service'; import { DadosUsuariosService, @@ -45,6 +47,7 @@ export class DadosUsuarios implements OnInit { @ViewChild('successToast', { static: false }) successToast!: ElementRef; loading = false; + exporting = false; errorMsg = ''; // Filtros @@ -116,7 +119,8 @@ export class DadosUsuarios implements OnInit { constructor( private service: DadosUsuariosService, private authService: AuthService, - private linesService: LinesService + private linesService: LinesService, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -256,6 +260,112 @@ export class DadosUsuarios implements OnInit { } clearFilters() { this.search = ''; this.fetch(1); } + + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = await this.fetchAllRowsForExport(); + const rows = await this.fetchDetailedRowsForExport(baseRows); + if (!rows.length) { + this.showToast('Nenhum registro encontrado para exportar.', 'danger'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + const fileName = `dados_usuarios_${this.tipoFilter.toLowerCase()}_${timestamp}`; + + await this.tableExportService.exportAsXlsx({ + fileName, + sheetName: 'DadosUsuarios', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Tipo', value: (row) => this.normalizeTipo(row) }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { + header: this.tipoFilter === 'PJ' ? 'Razao Social' : 'Nome', + value: (row) => (this.normalizeTipo(row) === 'PJ' ? (row.razaoSocial ?? row.cliente ?? '') : (row.nome ?? row.cliente ?? '')), + }, + { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, + { header: 'Linha', value: (row) => row.linha ?? '' }, + { header: 'CPF', value: (row) => row.cpf ?? '' }, + { header: 'CNPJ', value: (row) => row.cnpj ?? '' }, + { header: 'E-mail', value: (row) => row.email ?? '' }, + { header: 'Celular', value: (row) => row.celular ?? '' }, + { header: 'Telefone Fixo', value: (row) => row.telefoneFixo ?? '' }, + { header: 'RG', value: (row) => row.rg ?? '' }, + { header: 'Endereco', value: (row) => row.endereco ?? '' }, + { header: 'Data de Nascimento', type: 'date', value: (row) => row.dataNascimento ?? '' }, + ], + }); + + this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success'); + } catch { + this.showToast('Erro ao exportar planilha.', 'danger'); + } finally { + this.exporting = false; + } + } + + private async fetchAllRowsForExport(): Promise { + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: UserDataRow[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.service.getRows({ + search: this.search?.trim(), + tipo: this.tipoFilter, + page, + pageSize, + sortBy: 'item', + sortDir: 'asc', + }) + ); + + const items = response?.items ?? []; + expectedTotal = response?.total ?? 0; + all.push(...items); + + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && all.length >= expectedTotal) break; + page += 1; + } + + return all.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0); + }); + } + + private async fetchDetailedRowsForExport(rows: UserDataRow[]): Promise { + if (!rows.length) return []; + + const detailed: UserDataRow[] = []; + const chunkSize = 10; + + for (let i = 0; i < rows.length; i += chunkSize) { + const chunk = rows.slice(i, i + chunkSize); + const resolved = await Promise.all( + chunk.map(async (row) => { + try { + return await firstValueFrom(this.service.getById(row.id)); + } catch { + return row; + } + }) + ); + detailed.push(...resolved); + } + + return detailed; + } onPageSizeChange() { this.page = 1; diff --git a/src/app/pages/faturamento/faturamento.html b/src/app/pages/faturamento/faturamento.html index b8dc60b..0fdbf91 100644 --- a/src/app/pages/faturamento/faturamento.html +++ b/src/app/pages/faturamento/faturamento.html @@ -33,7 +33,12 @@ Totais, lucro e comparativo Vivo x Line -
+
+ +
@@ -184,7 +189,6 @@
-
diff --git a/src/app/pages/faturamento/faturamento.ts b/src/app/pages/faturamento/faturamento.ts index 0c46cbb..7046ebc 100644 --- a/src/app/pages/faturamento/faturamento.ts +++ b/src/app/pages/faturamento/faturamento.ts @@ -25,7 +25,9 @@ import { } from '../../services/billing'; import { AuthService } from '../../services/auth.service'; import { LinesService } from '../../services/lines.service'; +import { TableExportService } from '../../services/table-export.service'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; +import { firstValueFrom } from 'rxjs'; interface BillingClientGroup { cliente: string; @@ -54,10 +56,12 @@ export class Faturamento implements AfterViewInit, OnDestroy { private billing: BillingService, private linesService: LinesService, private cdr: ChangeDetectorRef, - private authService: AuthService + private authService: AuthService, + private tableExportService: TableExportService ) {} loading = false; + exporting = false; // filtros searchTerm = ''; @@ -415,6 +419,85 @@ export class Faturamento implements AfterViewInit, OnDestroy { this.loadAllAndApply(forceReloadAll); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = this.getRowsForExport(); + const rows = await this.fetchDetailedRowsForExport(baseRows); + if (!rows.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + const suffix = this.filterTipo === 'ALL' ? 'todos' : this.filterTipo.toLowerCase(); + await this.tableExportService.exportAsXlsx({ + fileName: `faturamento_${suffix}_${timestamp}`, + sheetName: 'Faturamento', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Tipo', value: (row) => row.tipo ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { header: 'Qtd Linhas', type: 'number', value: (row) => this.toNullableNumber(row.qtdLinhas) ?? 0 }, + { header: 'Franquia Vivo', type: 'number', value: (row) => this.toNullableNumber(row.franquiaVivo) ?? 0 }, + { header: 'Valor Contrato Vivo', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoVivo) ?? 0 }, + { header: 'Franquia Line', type: 'number', value: (row) => this.toNullableNumber(row.franquiaLine) ?? 0 }, + { header: 'Valor Contrato Line', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoLine) ?? 0 }, + { header: 'Lucro', type: 'currency', value: (row) => this.toNullableNumber(row.lucro) ?? 0 }, + { header: 'Aparelho', value: (row) => row.aparelho ?? '' }, + { header: 'Forma de Pagamento', value: (row) => row.formaPagamento ?? '' }, + { header: 'Observacao', value: (row) => this.getObservacao(row) }, + { header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + + private getRowsForExport(): BillingItem[] { + const rows: BillingItem[] = []; + this.rowsByClient.forEach((items) => rows.push(...items)); + + return rows.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0); + }); + } + + private async fetchDetailedRowsForExport(rows: BillingItem[]): Promise { + if (!rows.length) return []; + + const detailed: BillingItem[] = []; + const chunkSize = 10; + + for (let i = 0; i < rows.length; i += chunkSize) { + const chunk = rows.slice(i, i + chunkSize); + const resolved = await Promise.all( + chunk.map(async (row) => { + try { + return await firstValueFrom(this.billing.getById(row.id)); + } catch { + return row; + } + }) + ); + detailed.push(...resolved); + } + + return detailed; + } + private getAllItems(force = false): Promise { const now = Date.now(); @@ -795,4 +878,22 @@ export class Faturamento implements AfterViewInit, OnDestroy { const n = Number(value); return Number.isNaN(n) ? null : n; } + + private async showToast(message: string): Promise { + if (!isPlatformBrowser(this.platformId)) return; + this.toastMessage = message; + this.cdr.detectChanges(); + if (!this.successToast?.nativeElement) return; + + try { + const bs = await import('bootstrap'); + const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, { + autohide: true, + delay: 3000, + }); + toastInstance.show(); + } catch (error) { + console.error(error); + } + } } diff --git a/src/app/pages/geral/geral.html b/src/app/pages/geral/geral.html index 6ba0955..f34ddaf 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -31,22 +31,42 @@ Tabela de linhas e dados de telefonia -
- +
+
+ + + +
+ + +
+ +
+ + Selecionadas: {{ batchStatusSelectionCount }} + + + +
@@ -312,6 +352,20 @@ {{ reservaSelectedCount > 0 && reservaSelectedCount === groupLines.length ? 'Limpar seleção' : 'Selecionar todas' }} + + + + + + + + +
{ @@ -303,6 +338,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return `${apiBase}/templates`; })(); loading = false; + exporting = false; isSysAdmin = false; isGestor = false; isClientRestricted = false; @@ -378,6 +414,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { }; reservaTransferLastResult: AssignReservaLinesResultDto | null = null; moveToReservaLastResult: AssignReservaLinesResultDto | null = null; + batchStatusOpen = false; + batchStatusSaving = false; + batchStatusAction: BatchStatusAction = 'BLOCK'; + batchStatusType = ''; + batchStatusUsuario = ''; + batchStatusLastResult: BatchLineStatusUpdateResultDto | null = null; detailData: any = null; financeData: any = null; @@ -609,6 +651,46 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return this.hasGroupLineSelectionTools && !this.isReservaExpandedGroup && !this.isExpandedGroupNamedReserva; } + get blockedStatusOptions(): string[] { + return this.statusOptions.filter((status) => !this.isActiveStatus(status)); + } + + get batchStatusSelectionCount(): number { + return this.reservaSelectedCount; + } + + get canOpenBatchStatusModal(): boolean { + if (this.isClientRestricted) return false; + if (this.loading || this.batchStatusSaving) return false; + return this.batchStatusSelectionCount > 0; + } + + get canSubmitBatchStatusModal(): boolean { + if (this.batchStatusSaving) return false; + if (this.batchStatusSelectionCount <= 0) return false; + if (this.batchStatusAction === 'BLOCK' && !String(this.batchStatusType ?? '').trim()) return false; + return true; + } + + get batchStatusActionLabel(): string { + return this.batchStatusAction === 'BLOCK' ? 'Bloquear' : 'Desbloquear'; + } + + get batchStatusTargetDescription(): string { + return `${this.batchStatusSelectionCount} linha(s) selecionada(s)`; + } + + get batchStatusUserOptions(): string[] { + const users = (this.groupLines ?? []) + .map((x) => (x.usuario ?? '').toString().trim()) + .filter((x) => !!x); + + const current = (this.batchStatusUsuario ?? '').toString().trim(); + if (current) users.push(current); + + return Array.from(new Set(users)).sort((a, b) => a.localeCompare(b, 'pt-BR', { sensitivity: 'base' })); + } + get reservaSelectedCount(): number { return this.reservaSelectedLineIds.length; } @@ -823,7 +905,15 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { // ✅ FIX PRINCIPAL: limpeza forçada de backdrops/scroll lock // ============================================================ private anyModalOpen(): boolean { - return !!(this.detailOpen || this.financeOpen || this.editOpen || this.createOpen || this.reservaTransferOpen || this.moveToReservaOpen); + return !!( + this.detailOpen || + this.financeOpen || + this.editOpen || + this.createOpen || + this.reservaTransferOpen || + this.moveToReservaOpen || + this.batchStatusOpen + ); } private cleanupModalArtifacts() { @@ -851,6 +941,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.createOpen = false; this.reservaTransferOpen = false; this.moveToReservaOpen = false; + this.batchStatusOpen = false; this.detailData = null; this.financeData = null; @@ -869,8 +960,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.batchExcelTemplateDownloading = false; this.reservaTransferSaving = false; this.moveToReservaSaving = false; + this.batchStatusSaving = false; this.reservaTransferLastResult = null; this.moveToReservaLastResult = null; + this.batchStatusLastResult = null; + this.batchStatusUsuario = ''; // Limpa overlays/locks residuais this.cleanupModalArtifacts(); @@ -1800,6 +1894,225 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.refreshData(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = await this.getRowsForExport(); + const rows = await this.getDetailedRowsForExport(baseRows); + if (!rows.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const suffix = this.getExportFilterSuffix(); + const timestamp = this.tableExportService.buildTimestamp(); + const fileName = `geral_${suffix}_${timestamp}`; + const templateBuffer = await this.getGeralTemplateBuffer(); + + await this.tableExportService.exportAsXlsx({ + fileName, + sheetName: 'Geral', + templateBuffer, + rows, + columns: [ + { header: 'ID', value: (row) => row.id }, + { header: 'Item', type: 'number', value: (row) => this.toInt(row.item) }, + { header: 'Empresa (Conta)', value: (row) => this.findEmpresaByConta(row.conta) }, + { header: 'Conta', value: (row) => row.conta ?? '' }, + { header: 'Linha', value: (row) => row.linha ?? '' }, + { header: 'Chip', value: (row) => row.chip ?? '' }, + { header: 'Tipo de Chip', value: (row) => row.tipoDeChip ?? '' }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { header: 'Usuario', value: (row) => row.usuario ?? '' }, + { header: 'Centro de Custos', value: (row) => row.centroDeCustos ?? '' }, + { header: 'Setor ID', value: (row) => row.setorId ?? '' }, + { header: 'Setor', value: (row) => row.setorNome ?? '' }, + { header: 'Aparelho ID', value: (row) => row.aparelhoId ?? '' }, + { header: 'Aparelho', value: (row) => row.aparelhoNome ?? '' }, + { header: 'Cor do Aparelho', value: (row) => row.aparelhoCor ?? '' }, + { header: 'IMEI do Aparelho', value: (row) => row.aparelhoImei ?? '' }, + { header: 'NF do Aparelho (anexo)', type: 'boolean', value: (row) => !!row.aparelhoNotaFiscalTemArquivo }, + { header: 'Recibo do Aparelho (anexo)', type: 'boolean', value: (row) => !!row.aparelhoReciboTemArquivo }, + { header: 'Plano Contrato', value: (row) => row.planoContrato ?? '' }, + { header: 'Status', value: (row) => row.status ?? '' }, + { header: 'Tipo (Skil)', value: (row) => row.skil ?? '' }, + { header: 'Modalidade', value: (row) => row.modalidade ?? '' }, + { header: 'Cedente', value: (row) => row.cedente ?? '' }, + { header: 'Solicitante', value: (row) => row.solicitante ?? '' }, + { header: 'Data de Bloqueio', type: 'date', value: (row) => row.dataBloqueio ?? '' }, + { header: 'Data Entrega Operadora', type: 'date', value: (row) => row.dataEntregaOpera ?? '' }, + { header: 'Data Entrega Cliente', type: 'date', value: (row) => row.dataEntregaCliente ?? '' }, + { header: 'Dt. Efetivacao Servico', type: 'date', value: (row) => row.dtEfetivacaoServico ?? '' }, + { header: 'Dt. Termino Fidelizacao', type: 'date', value: (row) => row.dtTerminoFidelizacao ?? '' }, + { header: 'Vencimento da Conta', value: (row) => row.vencConta ?? '' }, + { header: 'Franquia Vivo', type: 'number', value: (row) => this.toNullableNumber(row.franquiaVivo) ?? 0 }, + { header: 'Valor Plano Vivo', type: 'currency', value: (row) => this.toNullableNumber(row.valorPlanoVivo) ?? 0 }, + { header: 'Gestao Voz e Dados', type: 'currency', value: (row) => this.toNullableNumber(row.gestaoVozDados) ?? 0 }, + { header: 'Skeelo', type: 'currency', value: (row) => this.toNullableNumber(row.skeelo) ?? 0 }, + { header: 'Vivo News Plus', type: 'currency', value: (row) => this.toNullableNumber(row.vivoNewsPlus) ?? 0 }, + { header: 'Vivo Travel Mundo', type: 'currency', value: (row) => this.toNullableNumber(row.vivoTravelMundo) ?? 0 }, + { header: 'Vivo Gestao Dispositivo', type: 'currency', value: (row) => this.toNullableNumber(row.vivoGestaoDispositivo) ?? 0 }, + { header: 'Vivo Sync', type: 'currency', value: (row) => this.toNullableNumber(row.vivoSync) ?? 0 }, + { header: 'Valor Contrato Vivo', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoVivo) ?? 0 }, + { header: 'Franquia Line', type: 'number', value: (row) => this.toNullableNumber(row.franquiaLine) ?? 0 }, + { header: 'Franquia Gestao', type: 'number', value: (row) => this.toNullableNumber(row.franquiaGestao) ?? 0 }, + { header: 'Locacao AP', type: 'currency', value: (row) => this.toNullableNumber(row.locacaoAp) ?? 0 }, + { header: 'Valor Contrato Line', type: 'currency', value: (row) => this.toNullableNumber(row.valorContratoLine) ?? 0 }, + { header: 'Desconto', type: 'currency', value: (row) => this.toNullableNumber(row.desconto) ?? 0 }, + { header: 'Lucro', type: 'currency', value: (row) => this.toNullableNumber(row.lucro) ?? 0 }, + { header: 'Criado Em', type: 'datetime', value: (row) => this.getAnyField(row, ['createdAt', 'CreatedAt']) ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => this.getAnyField(row, ['updatedAt', 'UpdatedAt']) ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar a planilha.'); + } finally { + this.exporting = false; + } + } + + private async getRowsForExport(): Promise { + let lines = await this.fetchLinesForGrouping(); + + if (this.selectedClients.length > 0) { + const selected = new Set( + this.selectedClients.map((client) => (client ?? '').toString().trim().toUpperCase()) + ); + lines = lines.filter((line) => selected.has((line.cliente ?? '').toString().trim().toUpperCase())); + } + + const fallbackClient = this.filterSkil === 'RESERVA' ? 'RESERVA' : 'SEM CLIENTE'; + const mapped = lines.map((line) => ({ + id: (line.id ?? '').toString(), + item: String(line.item ?? ''), + linha: line.linha ?? '', + chip: line.chip ?? '', + cliente: ((line.cliente ?? '').toString().trim()) || fallbackClient, + usuario: line.usuario ?? '', + centroDeCustos: line.centroDeCustos ?? '', + setorNome: line.setorNome ?? '', + aparelhoNome: line.aparelhoNome ?? '', + aparelhoCor: line.aparelhoCor ?? '', + status: line.status ?? '', + skil: line.skil ?? '', + contrato: line.vencConta ?? '', + })); + + return mapped.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + + const byItem = this.toInt(a.item) - this.toInt(b.item); + if (byItem !== 0) return byItem; + + return (a.linha ?? '').localeCompare(b.linha ?? '', 'pt-BR', { sensitivity: 'base' }); + }); + } + + private async getDetailedRowsForExport(baseRows: LineRow[]): Promise { + if (!baseRows.length) return []; + + const result: ApiLineDetail[] = []; + const chunkSize = 8; + + for (let i = 0; i < baseRows.length; i += chunkSize) { + const chunk = baseRows.slice(i, i + chunkSize); + const fetched = await Promise.all( + chunk.map(async (row) => { + try { + return await firstValueFrom( + this.http.get(`${this.apiBase}/${row.id}`, { + params: this.withNoCache(new HttpParams()), + }) + ); + } catch { + return this.toDetailFallback(row); + } + }) + ); + + result.push(...fetched); + } + + return result; + } + + private toDetailFallback(row: LineRow): ApiLineDetail { + return { + id: row.id, + item: this.toInt(row.item), + qtdLinhas: null, + conta: row.contrato ?? null, + linha: row.linha ?? null, + chip: row.chip ?? null, + tipoDeChip: null, + cliente: row.cliente ?? null, + usuario: row.usuario ?? null, + centroDeCustos: row.centroDeCustos ?? null, + setorId: null, + setorNome: row.setorNome ?? null, + aparelhoId: null, + aparelhoNome: row.aparelhoNome ?? null, + aparelhoCor: row.aparelhoCor ?? null, + aparelhoImei: null, + aparelhoNotaFiscalTemArquivo: false, + aparelhoReciboTemArquivo: false, + planoContrato: null, + status: row.status ?? null, + skil: row.skil ?? null, + modalidade: null, + dataBloqueio: null, + cedente: null, + solicitante: null, + dataEntregaOpera: null, + dataEntregaCliente: null, + dtEfetivacaoServico: null, + dtTerminoFidelizacao: null, + vencConta: row.contrato ?? null, + franquiaVivo: null, + valorPlanoVivo: null, + gestaoVozDados: null, + skeelo: null, + vivoNewsPlus: null, + vivoTravelMundo: null, + vivoGestaoDispositivo: null, + vivoSync: null, + valorContratoVivo: null, + franquiaLine: null, + franquiaGestao: null, + locacaoAp: null, + valorContratoLine: null, + desconto: null, + lucro: null, + }; + } + + private async getGeralTemplateBuffer(): Promise { + try { + const params = new HttpParams().set('_', `${Date.now()}`); + const blob = await firstValueFrom( + this.http.get(`${this.templatesApiBase}/planilha-geral`, { + params, + responseType: 'blob', + }) + ); + return await blob.arrayBuffer(); + } catch { + return null; + } + } + + private getExportFilterSuffix(): string { + if (this.filterSkil === 'PF') return 'pf'; + if (this.filterSkil === 'PJ') return 'pj'; + if (this.filterSkil === 'RESERVA') return 'reserva'; + return 'todas'; + } + async onImportExcel() { if (!this.isSysAdmin) { await this.showToast('Você não tem permissão para importar planilha.'); @@ -3167,6 +3480,116 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.reservaSelectedLineIds = []; } + async openBatchStatusModal(action: BatchStatusAction) { + if (this.isClientRestricted) { + await this.showToast('Você não tem permissão para bloquear/desbloquear em lote.'); + return; + } + + if (this.batchStatusSelectionCount <= 0) { + await this.showToast('Selecione ao menos uma linha para processar.'); + return; + } + + this.batchStatusAction = action; + this.batchStatusSaving = false; + this.batchStatusLastResult = null; + this.batchStatusUsuario = ''; + + if (action === 'BLOCK') { + const current = (this.batchStatusType ?? '').toString().trim(); + const options = this.blockedStatusOptions; + if (!current || !options.some((x) => x === current)) { + this.batchStatusType = options[0] ?? ''; + } + } else { + this.batchStatusType = ''; + } + + this.batchStatusOpen = true; + this.cdr.detectChanges(); + } + + async submitBatchStatusUpdate() { + if (this.batchStatusSaving) return; + if (!this.canSubmitBatchStatusModal) return; + + const payload = this.buildBatchStatusPayload(); + this.batchStatusSaving = true; + + this.http.post(`${this.apiBase}/batch-status-update`, payload).subscribe({ + next: async (res) => { + this.batchStatusSaving = false; + this.batchStatusLastResult = res; + + const ok = Number(res?.updated ?? 0) || 0; + const failed = Number(res?.failed ?? 0) || 0; + + this.batchStatusOpen = false; + this.clearReservaSelection(); + this.batchStatusUsuario = ''; + + await this.showToast( + failed > 0 + ? `${this.batchStatusActionLabel} em lote concluído com pendências: ${ok} linha(s) processada(s), ${failed} falha(s).` + : `${this.batchStatusActionLabel} em lote concluído: ${ok} linha(s) processada(s).` + ); + + if (this.expandedGroup) { + const term = (this.searchTerm ?? '').trim(); + const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined; + this.fetchGroupLines(this.expandedGroup, useTerm); + } + + this.loadGroups(); + this.loadKpis(); + }, + error: async (err: HttpErrorResponse) => { + this.batchStatusSaving = false; + const msg = (err.error as any)?.message || 'Erro ao processar bloqueio/desbloqueio em lote.'; + await this.showToast(msg); + } + }); + } + + private buildBatchStatusPayload(): BatchLineStatusUpdateRequestDto { + const clients = this.searchResolvedClient + ? [this.searchResolvedClient] + : [...this.selectedClients]; + + const normalizedClients = clients + .map((x) => (x ?? '').toString().trim()) + .filter((x) => !!x); + + const userFilter = (this.batchStatusUsuario ?? '').toString().trim(); + + return { + action: this.batchStatusAction === 'BLOCK' ? 'block' : 'unblock', + blockStatus: this.batchStatusAction === 'BLOCK' ? (this.batchStatusType || null) : null, + applyToAllFiltered: false, + lineIds: [...this.reservaSelectedLineIds], + search: (this.searchTerm ?? '').toString().trim() || null, + skil: this.resolveFilterSkilForApi(), + clients: normalizedClients, + additionalMode: this.resolveAdditionalModeForApi(), + additionalServices: this.selectedAdditionalServices.length > 0 ? this.selectedAdditionalServices.join(',') : null, + usuario: userFilter || null + }; + } + + private resolveFilterSkilForApi(): string | null { + if (this.filterSkil === 'PF') return 'PESSOA FÍSICA'; + if (this.filterSkil === 'PJ') return 'PESSOA JURÍDICA'; + if (this.filterSkil === 'RESERVA') return 'RESERVA'; + return null; + } + + private resolveAdditionalModeForApi(): string | null { + if (this.additionalMode === 'WITH') return 'with'; + if (this.additionalMode === 'WITHOUT') return 'without'; + return null; + } + async openReservaTransferModal() { if (!this.isReservaExpandedGroup) { await this.showToast('Abra um grupo no filtro Reserva para selecionar e atribuir linhas.'); @@ -3588,6 +4011,12 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return v || '-'; } + private isActiveStatus(status: string | null | undefined): boolean { + const normalized = (status ?? '').toString().trim().toLowerCase(); + if (!normalized) return false; + return normalized.includes('ativo'); + } + private toEditModel(d: ApiLineDetail): any { return { ...d, @@ -3660,6 +4089,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return Number.isNaN(n) ? null : n; } + private getAnyField(row: unknown, keys: string[]): unknown { + const source = row as Record; + for (const key of keys) { + if (source && source[key] !== undefined && source[key] !== null && source[key] !== '') { + return source[key]; + } + } + return null; + } + private mergeOption(current: any, list: string[]): string[] { const v = (current ?? '').toString().trim(); if (!v) return list; diff --git a/src/app/pages/historico/historico.html b/src/app/pages/historico/historico.html index e01f1e8..9cc9940 100644 --- a/src/app/pages/historico/historico.html +++ b/src/app/pages/historico/historico.html @@ -33,6 +33,10 @@ +
diff --git a/src/app/pages/historico/historico.ts b/src/app/pages/historico/historico.ts index 5ce35cf..16fe62c 100644 --- a/src/app/pages/historico/historico.ts +++ b/src/app/pages/historico/historico.ts @@ -2,9 +2,11 @@ import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PL import { CommonModule, isPlatformBrowser } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { HistoricoService, AuditLogDto, AuditChangeType, HistoricoQuery } from '../../services/historico.service'; +import { TableExportService } from '../../services/table-export.service'; interface SelectOption { value: string; @@ -23,6 +25,7 @@ export class Historico implements OnInit { logs: AuditLogDto[] = []; loading = false; + exporting = false; error = false; errorMsg = ''; toastMessage = ''; @@ -65,7 +68,8 @@ export class Historico implements OnInit { constructor( private historicoService: HistoricoService, private cdr: ChangeDetectorRef, - @Inject(PLATFORM_ID) private platformId: object + @Inject(PLATFORM_ID) private platformId: object, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -111,6 +115,47 @@ export class Historico implements OnInit { this.fetch(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const logs = await this.fetchAllLogsForExport(); + if (!logs.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `historico_${timestamp}`, + sheetName: 'Historico', + rows: logs, + columns: [ + { header: 'ID', value: (log) => log.id ?? '' }, + { header: 'Data/Hora', type: 'datetime', value: (log) => log.occurredAtUtc ?? '' }, + { header: 'Usuario', value: (log) => this.displayUserName(log) }, + { header: 'E-mail', value: (log) => log.userEmail ?? '' }, + { header: 'Pagina', value: (log) => log.page ?? '' }, + { header: 'Acao', value: (log) => this.formatAction(log.action) }, + { header: 'Entidade', value: (log) => this.displayEntity(log) }, + { header: 'Id Entidade', value: (log) => log.entityId ?? '' }, + { header: 'Metodo HTTP', value: (log) => log.requestMethod ?? '' }, + { header: 'Endpoint', value: (log) => log.requestPath ?? '' }, + { header: 'IP', value: (log) => log.ipAddress ?? '' }, + { header: 'Mudancas', value: (log) => this.formatChangesSummary(log) }, + { header: 'Qtd Mudancas', type: 'number', value: (log) => log.changes?.length ?? 0 }, + ], + }); + + await this.showToast(`Planilha exportada com ${logs.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + goToPage(p: number): void { this.page = Math.max(1, Math.min(this.totalPages, p)); this.fetch(); @@ -217,14 +262,9 @@ export class Historico implements OnInit { this.expandedLogId = null; const query: HistoricoQuery = { + ...this.buildBaseQuery(), page: this.page, pageSize: this.pageSize, - pageName: this.filterPageName || undefined, - action: this.filterAction || undefined, - user: this.filterUser?.trim() || undefined, - search: this.filterSearch?.trim() || undefined, - dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, - dateTo: this.toIsoDate(this.dateTo, true) || undefined, }; this.historicoService.list(query).subscribe({ @@ -247,6 +287,58 @@ export class Historico implements OnInit { }); } + private async fetchAllLogsForExport(): Promise { + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: AuditLogDto[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.historicoService.list({ + ...this.buildBaseQuery(), + page, + pageSize, + }) + ); + + const items = response?.items ?? []; + expectedTotal = response?.total ?? 0; + all.push(...items); + + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && all.length >= expectedTotal) break; + page += 1; + } + + return all; + } + + private buildBaseQuery(): Omit { + return { + pageName: this.filterPageName || undefined, + action: this.filterAction || undefined, + user: this.filterUser?.trim() || undefined, + search: this.filterSearch?.trim() || undefined, + dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, + dateTo: this.toIsoDate(this.dateTo, true) || undefined, + }; + } + + private formatChangesSummary(log: AuditLogDto): string { + const changes = log?.changes ?? []; + if (!changes.length) return ''; + return changes + .map((change) => { + const field = change?.field ?? 'campo'; + const oldValue = this.formatChangeValue(change?.oldValue); + const newValue = this.formatChangeValue(change?.newValue); + return `${field}: ${oldValue} -> ${newValue}`; + }) + .join(' | '); + } + private toIsoDate(value: string, endOfDay: boolean): string | null { if (!value) return null; const time = endOfDay ? '23:59:59' : '00:00:00'; diff --git a/src/app/pages/mureg/mureg.html b/src/app/pages/mureg/mureg.html index 58c0764..9a4cfa7 100644 --- a/src/app/pages/mureg/mureg.html +++ b/src/app/pages/mureg/mureg.html @@ -31,6 +31,10 @@
+ diff --git a/src/app/pages/mureg/mureg.ts b/src/app/pages/mureg/mureg.ts index a09f8f8..86fdc7b 100644 --- a/src/app/pages/mureg/mureg.ts +++ b/src/app/pages/mureg/mureg.ts @@ -10,8 +10,10 @@ import { import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { LinesService } from '../../services/lines.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { TableExportService } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; @@ -75,6 +77,17 @@ interface MuregDetailDto { statusNaGeral: string | null; } +type MuregExportRow = MuregRow & { + usuario?: string | null; + skil?: string | null; + linhaAtualNaGeral?: string | null; + chipNaGeral?: string | null; + contaNaGeral?: string | null; + statusNaGeral?: string | null; + createdAt?: string | null; + updatedAt?: string | null; +}; + @Component({ standalone: true, imports: [CommonModule, FormsModule, CustomSelectComponent], @@ -84,6 +97,7 @@ interface MuregDetailDto { export class Mureg implements AfterViewInit { toastMessage = ''; loading = false; + exporting = false; @ViewChild('successToast', { static: false }) successToast!: ElementRef; @@ -91,7 +105,8 @@ export class Mureg implements AfterViewInit { @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, private cdr: ChangeDetectorRef, - private linesService: LinesService + private linesService: LinesService, + private tableExportService: TableExportService ) {} private readonly apiBase = (() => { @@ -184,6 +199,147 @@ export class Mureg implements AfterViewInit { this.loadForGroups(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = await this.fetchAllRowsForExport(); + const rows = await this.fetchDetailedRowsForExport(baseRows); + if (!rows.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `mureg_${timestamp}`, + sheetName: 'Mureg', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Cliente', value: (row) => row.cliente }, + { header: 'Usuario', value: (row) => row.usuario ?? '' }, + { header: 'Skil', value: (row) => row.skil ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toIntOrZero(row.item) }, + { header: 'Linha Antiga', value: (row) => row.linhaAntiga }, + { header: 'Linha Nova', value: (row) => row.linhaNova }, + { header: 'ICCID', value: (row) => row.iccid }, + { header: 'Data da Mureg', type: 'date', value: (row) => row.dataDaMureg }, + { header: 'Situacao', value: (row) => (this.isTroca(row) ? 'TROCA' : 'SEM TROCA') }, + { header: 'Linha ID (Geral)', value: (row) => row.mobileLineId ?? '' }, + { header: 'Linha Atual na Geral', value: (row) => row.linhaAtualNaGeral ?? '' }, + { header: 'Chip na Geral', value: (row) => row.chipNaGeral ?? '' }, + { header: 'Conta na Geral', value: (row) => row.contaNaGeral ?? '' }, + { header: 'Status na Geral', value: (row) => row.statusNaGeral ?? '' }, + { header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + + private async fetchAllRowsForExport(): Promise { + const pageSize = 2000; + let page = 1; + let expectedTotal = 0; + const rows: MuregRow[] = []; + + while (page <= 500) { + const params = new HttpParams() + .set('page', String(page)) + .set('pageSize', String(pageSize)) + .set('search', (this.searchTerm ?? '').trim()) + .set('sortBy', 'cliente') + .set('sortDir', 'asc'); + + const response = await firstValueFrom( + this.http.get | any[]>(this.apiBase, { params }) + ); + + const items = Array.isArray(response) ? response : (response.items ?? []); + const normalized = items.map((item: any, idx: number) => this.normalizeRow(item, rows.length + idx)); + rows.push(...normalized); + expectedTotal = Array.isArray(response) ? 0 : Number(response.total ?? 0); + + if (Array.isArray(response)) break; + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && rows.length >= expectedTotal) break; + page += 1; + } + + return rows.sort((a, b) => { + const byClient = (a.cliente ?? '').localeCompare(b.cliente ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byClient !== 0) return byClient; + + const byItem = this.toIntOrZero(a.item) - this.toIntOrZero(b.item); + if (byItem !== 0) return byItem; + + return (a.linhaNova ?? '').localeCompare(b.linhaNova ?? '', 'pt-BR', { sensitivity: 'base' }); + }); + } + + private async fetchDetailedRowsForExport(rows: MuregRow[]): Promise { + if (!rows.length) return []; + + const result: MuregExportRow[] = []; + const chunkSize = 10; + + for (let i = 0; i < rows.length; i += chunkSize) { + const chunk = rows.slice(i, i + chunkSize); + const detailedChunk = await Promise.all( + chunk.map(async (row) => { + try { + const detail = await firstValueFrom(this.http.get(`${this.apiBase}/${row.id}`)); + const merged: MuregExportRow = { + ...row, + item: detail.item !== undefined && detail.item !== null ? String(detail.item) : row.item, + linhaAntiga: detail.linhaAntiga ?? row.linhaAntiga, + linhaNova: detail.linhaNova ?? row.linhaNova, + iccid: detail.iccid ?? row.iccid, + dataDaMureg: detail.dataDaMureg ?? row.dataDaMureg, + cliente: detail.cliente ?? row.cliente, + mobileLineId: detail.mobileLineId ?? row.mobileLineId, + usuario: detail.usuario ?? null, + skil: detail.skil ?? null, + linhaAtualNaGeral: detail.linhaAtualNaGeral ?? null, + chipNaGeral: detail.chipNaGeral ?? null, + contaNaGeral: detail.contaNaGeral ?? null, + statusNaGeral: detail.statusNaGeral ?? null, + createdAt: this.getRawField(detail, ['createdAt', 'CreatedAt']) ?? this.getRawField(row.raw, ['createdAt', 'CreatedAt']), + updatedAt: this.getRawField(detail, ['updatedAt', 'UpdatedAt']) ?? this.getRawField(row.raw, ['updatedAt', 'UpdatedAt']), + }; + + return merged; + } catch { + return { + ...row, + usuario: this.getRawField(row.raw, ['usuario', 'Usuario']), + skil: this.getRawField(row.raw, ['skil', 'Skil']), + linhaAtualNaGeral: this.getRawField(row.raw, ['linhaAtualNaGeral', 'LinhaAtualNaGeral']), + chipNaGeral: this.getRawField(row.raw, ['chipNaGeral', 'ChipNaGeral']), + contaNaGeral: this.getRawField(row.raw, ['contaNaGeral', 'ContaNaGeral']), + statusNaGeral: this.getRawField(row.raw, ['statusNaGeral', 'StatusNaGeral']), + createdAt: this.getRawField(row.raw, ['createdAt', 'CreatedAt']), + updatedAt: this.getRawField(row.raw, ['updatedAt', 'UpdatedAt']), + }; + } + }) + ); + + result.push(...detailedChunk); + } + + return result; + } + onSearch() { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { @@ -770,6 +926,15 @@ export class Mureg implements AfterViewInit { } } + private getRawField(source: any, keys: string[]): string | null { + for (const key of keys) { + const value = source?.[key]; + if (value === undefined || value === null || String(value).trim() === '') continue; + return String(value); + } + return null; + } + displayValue(key: MuregKey, v: any): string { if (v === null || v === undefined || String(v).trim() === '') return '-'; diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss index f1c8de2..cd072b7 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss +++ b/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss @@ -429,14 +429,14 @@ display: inline-flex; align-items: center; gap: 8px; +} - span { - color: var(--pg-text-soft, #64748b); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 800; - } +.page-size span { + color: var(--pg-text-soft, #64748b); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 800; } .select-glass { diff --git a/src/app/pages/parcelamentos/parcelamentos.html b/src/app/pages/parcelamentos/parcelamentos.html index 1697b20..78ba26f 100644 --- a/src/app/pages/parcelamentos/parcelamentos.html +++ b/src/app/pages/parcelamentos/parcelamentos.html @@ -1,4 +1,14 @@
+
+
+
+ LineGestao + +
+
{{ toastMessage }}
+
+
+
diff --git a/src/app/pages/resumo/resumo.ts b/src/app/pages/resumo/resumo.ts index 3a7706d..a2c04d2 100644 --- a/src/app/pages/resumo/resumo.ts +++ b/src/app/pages/resumo/resumo.ts @@ -31,6 +31,7 @@ import { ReservaPorDdd, ReservaTotal } from '../../services/resumo.service'; +import { TableExportService, type ExportCellType } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva'; @@ -85,6 +86,11 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { loading = false; errorMessage = ''; + toastOpen = false; + toastMessage = ''; + toastType: 'success' | 'danger' = 'success'; + private toastTimer: ReturnType | null = null; + private exportingKeys = new Set(); resumo: ResumoResponse | null = null; activeTab: ResumoTab = 'planos'; @@ -139,7 +145,8 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { private resumoService: ResumoService, private route: ActivatedRoute, private router: Router, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + private tableExportService: TableExportService ) { const raw = (environment.apiUrl || '').replace(/\/+$/, ''); this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; @@ -172,6 +179,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { ngOnDestroy(): void { Object.values(this.charts).forEach(c => c?.destroy()); + if (this.toastTimer) clearTimeout(this.toastTimer); } setTab(tab: ResumoTab): void { @@ -644,7 +652,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { this.updateGroupView(g); } toggleGroupedCompact(g: GroupedTableState) { g.compact = !g.compact; } - exportGroupedCsv(g: GroupedTableState, file: string) { this.exportCsv(g.table, file); } + exportGroupedCsv(g: GroupedTableState, file: string) { void this.exportTableAsXlsx(g.table, file); } isGroupedOpen(g: GroupedTableState, key: string) { return g.open.has(key); } toggleGroupedOpen(g: GroupedTableState, key: string) { if (g.open.has(key)) g.open.delete(key); else g.open.add(key); } openGroupedDetail(g: GroupedTableState, item: GroupItem) { g.detailGroup = item; g.detailOpen = true; } @@ -677,6 +685,10 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { return normalized === 'true' || normalized === '1' || normalized === 'sim'; } + isExporting(key: string): boolean { + return this.exportingKeys.has(key); + } + private initTables() { const hideMoneyColumns = (cols: TableColumn[]) => this.showFinancial ? cols : cols.filter((c) => c.type !== 'money'); @@ -1214,78 +1226,59 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { return Number.isNaN(parsed) ? null : parsed; } - private exportCsv(table: TableState, filename: string) { + private async exportTableAsXlsx(table: TableState, fileKey: string): Promise { if (!isPlatformBrowser(this.platformId)) return; + if (this.exportingKeys.has(fileKey)) return; + const rows = table.data ?? []; - const columns = table.columns ?? []; - const generatedAt = new Date().toLocaleString('pt-BR'); - const escapeHtml = (value: string) => - value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + if (!rows.length) { + this.showToast('Nenhum registro encontrado para exportar.', 'danger'); + return; + } - const headerHtml = columns - .map((column) => `${escapeHtml(column.label)}`) - .join(''); + this.exportingKeys.add(fileKey); + try { + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `${fileKey}_${timestamp}`, + sheetName: table.label || 'Resumo', + rows, + columns: (table.columns ?? []).map((column) => ({ + header: column.label, + type: this.mapColumnType(column.type), + value: (row: T) => this.getExportColumnValue(column, row), + })), + }); - const bodyHtml = rows - .map((row, index) => { - const cells = columns - .map((column) => { - const value = this.formatCell(column, row); - const toneClass = column.tone ? this.getToneClass(column.value(row)) : ''; - const alignClass = column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : ''; - const classes = [alignClass, toneClass].filter(Boolean).join(' '); - return `${escapeHtml(String(value))}`; - }) - .join(''); - return `${cells}`; - }) - .join(''); + this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success'); + } catch { + this.showToast('Erro ao exportar planilha.', 'danger'); + } finally { + this.exportingKeys.delete(fileKey); + } + } - const html = ` - - - - - - -
${escapeHtml(table.label || 'Resumo')}
-
Exportado em ${escapeHtml(generatedAt)} | Total de linhas: ${rows.length}
- - - ${headerHtml} - - - ${bodyHtml} - -
- -`; + private getExportColumnValue(column: TableColumn, row: T): unknown { + const rawValue = column.value(row); + if (column.type === 'money' || column.type === 'number' || column.type === 'gb') { + const numeric = this.toNumber(rawValue); + if (numeric !== null) return numeric; + } + return this.formatCell(column, row); + } - const blob = new Blob([`\uFEFF${html}`], { type: 'application/vnd.ms-excel;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${filename}.xls`; - a.click(); - URL.revokeObjectURL(url); + private mapColumnType(type: TableColumn['type']): ExportCellType { + if (type === 'money') return 'currency'; + if (type === 'number' || type === 'gb') return 'number'; + return 'text'; + } + + private showToast(message: string, type: 'success' | 'danger'): void { + this.toastMessage = message; + this.toastType = type; + this.toastOpen = true; + if (this.toastTimer) clearTimeout(this.toastTimer); + this.toastTimer = setTimeout(() => (this.toastOpen = false), 3000); } private getReservaPorDddChartData(): Array<{ label: string; totalLinhas: number }> { @@ -1310,7 +1303,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { return Array.from(map.entries()).map(([label, totalLinhas]) => ({ label, totalLinhas })); } - exportMacrophonyCsv() { this.exportCsv(this.tableMacrophony, 'macrophony-planos'); } + exportMacrophonyCsv() { void this.exportTableAsXlsx(this.tableMacrophony, 'macrophony-planos'); } findLineTotal(k: string[]): LineTotal | null { const keys = k.map((item) => item.toUpperCase()); const list = this.getEffectiveLineTotais(); diff --git a/src/app/pages/troca-numero/troca-numero.html b/src/app/pages/troca-numero/troca-numero.html index 6e31ffb..0edff36 100644 --- a/src/app/pages/troca-numero/troca-numero.html +++ b/src/app/pages/troca-numero/troca-numero.html @@ -31,6 +31,10 @@
+ @@ -86,7 +90,6 @@ Itens por pág:
-
diff --git a/src/app/pages/troca-numero/troca-numero.ts b/src/app/pages/troca-numero/troca-numero.ts index b27b637..8eda5bc 100644 --- a/src/app/pages/troca-numero/troca-numero.ts +++ b/src/app/pages/troca-numero/troca-numero.ts @@ -10,7 +10,9 @@ import { import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { TableExportService } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao'; @@ -63,13 +65,15 @@ interface LineOptionDto { export class TrocaNumero implements AfterViewInit { toastMessage = ''; loading = false; + exporting = false; @ViewChild('successToast', { static: false }) successToast!: ElementRef; constructor( @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + private tableExportService: TableExportService ) {} private readonly apiBase = (() => { @@ -151,6 +155,90 @@ export class TrocaNumero implements AfterViewInit { this.loadForGroups(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const rows = await this.fetchAllRowsForExport(); + if (!rows.length) { + await this.showToast('Nenhum registro encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `troca_numero_${timestamp}`, + sheetName: 'TrocaNumero', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Motivo', value: (row) => row.motivo }, + { header: 'Cliente', value: (row) => this.getRawField(row, ['cliente', 'Cliente']) ?? '' }, + { header: 'Usuario', value: (row) => this.getRawField(row, ['usuario', 'Usuario']) ?? '' }, + { header: 'Skil', value: (row) => this.getRawField(row, ['skil', 'Skil']) ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toNumberOrNull(row.item) ?? 0 }, + { header: 'Linha Antiga', value: (row) => row.linhaAntiga }, + { header: 'Linha Nova', value: (row) => row.linhaNova }, + { header: 'ICCID', value: (row) => row.iccid }, + { header: 'Data da Troca', type: 'date', value: (row) => row.dataTroca }, + { header: 'Observacao', value: (row) => row.observacao }, + { header: 'Situacao', value: (row) => (this.isTroca(row) ? 'TROCA' : 'SEM TROCA') }, + { header: 'Linha ID (Geral)', value: (row) => this.getRawField(row, ['mobileLineId', 'MobileLineId']) ?? '' }, + { header: 'Criado Em', type: 'datetime', value: (row) => this.getRawField(row, ['createdAt', 'CreatedAt']) ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => this.getRawField(row, ['updatedAt', 'UpdatedAt']) ?? '' }, + ], + }); + + await this.showToast(`Planilha exportada com ${rows.length} registro(s).`); + } catch { + await this.showToast('Erro ao exportar planilha.'); + } finally { + this.exporting = false; + } + } + + private async fetchAllRowsForExport(): Promise { + const pageSize = 2000; + let page = 1; + let expectedTotal = 0; + const rows: TrocaRow[] = []; + + while (page <= 500) { + const params = new HttpParams() + .set('page', String(page)) + .set('pageSize', String(pageSize)) + .set('search', (this.searchTerm ?? '').trim()) + .set('sortBy', 'motivo') + .set('sortDir', 'asc'); + + const response = await firstValueFrom( + this.http.get | any[]>(this.apiBase, { params }) + ); + + const items = Array.isArray(response) ? response : (response.items ?? []); + const normalized = items.map((item: any, idx: number) => this.normalizeRow(item, rows.length + idx)); + rows.push(...normalized); + expectedTotal = Array.isArray(response) ? 0 : Number(response.total ?? 0); + + if (Array.isArray(response)) break; + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && rows.length >= expectedTotal) break; + page += 1; + } + + return rows.sort((a, b) => { + const byMotivo = (a.motivo ?? '').localeCompare(b.motivo ?? '', 'pt-BR', { sensitivity: 'base' }); + if (byMotivo !== 0) return byMotivo; + + const byItem = (this.toNumberOrNull(a.item) ?? 0) - (this.toNumberOrNull(b.item) ?? 0); + if (byItem !== 0) return byItem; + + return (a.linhaNova ?? '').localeCompare(b.linhaNova ?? '', 'pt-BR', { sensitivity: 'base' }); + }); + } + onSearch() { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(() => { @@ -542,6 +630,15 @@ export class TrocaNumero implements AfterViewInit { return Number.isFinite(n) ? n : null; } + private getRawField(row: TrocaRow, keys: string[]): string | null { + for (const key of keys) { + const value = row?.raw?.[key]; + if (value === undefined || value === null || String(value).trim() === '') continue; + return String(value); + } + return null; + } + private isoToDateInput(iso: string | null | undefined): string { if (!iso) return ''; const dt = new Date(iso); diff --git a/src/app/pages/vigencia/vigencia.html b/src/app/pages/vigencia/vigencia.html index 1ae2d25..1b92ea9 100644 --- a/src/app/pages/vigencia/vigencia.html +++ b/src/app/pages/vigencia/vigencia.html @@ -24,6 +24,10 @@ Controle de contratos e fidelização
+ diff --git a/src/app/pages/vigencia/vigencia.ts b/src/app/pages/vigencia/vigencia.ts index 31d1148..5b10bbe 100644 --- a/src/app/pages/vigencia/vigencia.ts +++ b/src/app/pages/vigencia/vigencia.ts @@ -3,12 +3,13 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; -import { Subscription } from 'rxjs'; +import { Subscription, firstValueFrom } from 'rxjs'; import { VigenciaService, VigenciaRow, VigenciaClientGroup, PagedResult, UpdateVigenciaRequest } from '../../services/vigencia.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { AuthService } from '../../services/auth.service'; import { LinesService, MobileLineDetail } from '../../services/lines.service'; import { PlanAutoFillService } from '../../services/plan-autofill.service'; +import { TableExportService } from '../../services/table-export.service'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; type SortDir = 'asc' | 'desc'; @@ -32,6 +33,7 @@ interface LineOptionDto { }) export class VigenciaComponent implements OnInit, OnDestroy { loading = false; + exporting = false; errorMsg = ''; // Filtros @@ -113,7 +115,8 @@ export class VigenciaComponent implements OnInit, OnDestroy { private authService: AuthService, private linesService: LinesService, private planAutoFill: PlanAutoFillService, - private route: ActivatedRoute + private route: ActivatedRoute, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -295,6 +298,107 @@ export class VigenciaComponent implements OnInit, OnDestroy { this.fetch(1); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = await this.fetchAllRowsForExport(); + const rows = await this.fetchDetailedRowsForExport(baseRows); + if (!rows.length) { + this.showToast('Nenhum registro encontrado para exportar.', 'danger'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `vigencia_${timestamp}`, + sheetName: 'Vigencia', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, + { header: 'Linha', value: (row) => row.linha ?? '' }, + { header: 'Conta', value: (row) => row.conta ?? '' }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { header: 'Usuario', value: (row) => row.usuario ?? '' }, + { header: 'Plano', value: (row) => row.planoContrato ?? '' }, + { header: 'Efetivacao', type: 'date', value: (row) => row.dtEfetivacaoServico ?? '' }, + { header: 'Termino Fidelizacao', type: 'date', value: (row) => row.dtTerminoFidelizacao ?? '' }, + { header: 'Status', value: (row) => (this.isVencido(row.dtTerminoFidelizacao) ? 'Vencido' : 'Ativo') }, + { header: 'Auto Renovacao (anos)', type: 'number', value: (row) => this.toNullableNumber(row.autoRenewYears) ?? 0 }, + { header: 'Auto Renovacao Referencia', type: 'date', value: (row) => row.autoRenewReferenceEndDate ?? '' }, + { header: 'Auto Renovacao Configurada Em', type: 'datetime', value: (row) => row.autoRenewConfiguredAt ?? '' }, + { header: 'Ultima Auto Renovacao', type: 'datetime', value: (row) => row.lastAutoRenewedAt ?? '' }, + { header: 'Total', type: 'currency', value: (row) => this.toNullableNumber(row.total) ?? 0 }, + { header: 'Criado Em', type: 'datetime', value: (row) => row.createdAt ?? '' }, + { header: 'Atualizado Em', type: 'datetime', value: (row) => row.updatedAt ?? '' }, + ], + }); + + this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success'); + } catch { + this.showToast('Erro ao exportar planilha.', 'danger'); + } finally { + this.exporting = false; + } + } + + private async fetchAllRowsForExport(): Promise { + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: VigenciaRow[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.vigenciaService.getVigencia({ + search: this.search?.trim(), + client: this.client?.trim(), + page, + pageSize, + sortBy: 'item', + sortDir: 'asc', + }) + ); + + const items = response?.items ?? []; + expectedTotal = response?.total ?? 0; + all.push(...items); + + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && all.length >= expectedTotal) break; + page += 1; + } + + return all; + } + + private async fetchDetailedRowsForExport(rows: VigenciaRow[]): Promise { + if (!rows.length) return []; + + const detailedRows: VigenciaRow[] = []; + const chunkSize = 10; + + for (let i = 0; i < rows.length; i += chunkSize) { + const chunk = rows.slice(i, i + chunkSize); + const resolved = await Promise.all( + chunk.map(async (row) => { + try { + return await firstValueFrom(this.vigenciaService.getById(row.id)); + } catch { + return row; + } + }) + ); + + detailedRows.push(...resolved); + } + + return detailedRows; + } + scheduleAutoRenew(row: VigenciaRow): void { if (!row?.id) return; const years = 2; diff --git a/src/app/services/table-export.service.ts b/src/app/services/table-export.service.ts new file mode 100644 index 0000000..285ed51 --- /dev/null +++ b/src/app/services/table-export.service.ts @@ -0,0 +1,462 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../environments/environment'; + +export type ExportCellType = 'text' | 'number' | 'currency' | 'date' | 'datetime' | 'boolean'; + +export interface TableExportColumn { + header: string; + key?: string; + type?: ExportCellType; + width?: number; + value: (row: T, index: number) => unknown; +} + +export interface TableExportRequest { + fileName: string; + sheetName?: string; + columns: TableExportColumn[]; + rows: T[]; + templateBuffer?: ArrayBuffer | null; +} + +type CellStyleSnapshot = { + font?: Partial; + fill?: import('exceljs').Fill; + border?: Partial; + alignment?: Partial; +}; + +type TemplateStyleSnapshot = { + headerStyles: CellStyleSnapshot[]; + bodyStyle?: CellStyleSnapshot; + bodyAltStyle?: CellStyleSnapshot; + columnWidths: Array; +}; + +@Injectable({ providedIn: 'root' }) +export class TableExportService { + private readonly templatesApiBase = (() => { + const raw = (environment.apiUrl || '').replace(/\/+$/, ''); + const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + return `${apiBase}/templates`; + })(); + private defaultTemplateBufferPromise: Promise | null = null; + private cachedDefaultTemplateStyle?: TemplateStyleSnapshot; + + constructor(private readonly http: HttpClient) {} + + async exportAsXlsx(request: TableExportRequest): Promise { + const ExcelJS = await import('exceljs'); + const templateBuffer = request.templateBuffer ?? (await this.getDefaultTemplateBuffer()); + const templateStyle = await this.resolveTemplateStyle(ExcelJS, templateBuffer); + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet(this.sanitizeSheetName(request.sheetName || 'Dados')); + + const rawColumns = request.columns ?? []; + const columns = rawColumns.filter((column) => !this.shouldExcludeColumnByHeader(column.header)); + const rows = request.rows ?? []; + + if (!columns.length) { + throw new Error('Nenhuma coluna exportavel apos remover ITEM/ID.'); + } + + const headerValues = columns.map((c) => c.header ?? ''); + sheet.addRow(headerValues); + + rows.forEach((row, rowIndex) => { + const values = columns.map((column) => this.normalizeValue(column.value(row, rowIndex), column.type)); + sheet.addRow(values); + }); + + this.applyHeaderStyle(sheet, columns.length, templateStyle); + this.applyBodyStyle(sheet, columns, rows.length, templateStyle); + this.applyColumnWidths(sheet, columns, rows, templateStyle); + this.applyAutoFilter(sheet, columns.length); + sheet.views = [{ state: 'frozen', ySplit: 1 }]; + + const extensionSafeName = this.ensureXlsxExtension(request.fileName); + const buffer = await workbook.xlsx.writeBuffer(); + this.downloadBuffer(buffer, extensionSafeName); + } + + buildTimestamp(date: Date = new Date()): string { + const year = date.getFullYear(); + const month = this.pad2(date.getMonth() + 1); + const day = this.pad2(date.getDate()); + const hour = this.pad2(date.getHours()); + const minute = this.pad2(date.getMinutes()); + return `${year}-${month}-${day}_${hour}-${minute}`; + } + + private applyHeaderStyle( + sheet: import('exceljs').Worksheet, + columnCount: number, + templateStyle?: TemplateStyleSnapshot, + ): void { + const headerRow = sheet.getRow(1); + headerRow.height = 24; + + for (let col = 1; col <= columnCount; col += 1) { + const cell = headerRow.getCell(col); + const templateCell = this.getTemplateStyleByIndex(templateStyle, col - 1); + cell.font = this.cloneStyle(templateCell?.font) || { bold: true, color: { argb: 'FFFFFFFF' }, name: 'Calibri', size: 11 }; + cell.fill = this.cloneStyle(templateCell?.fill) || { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF0A58CA' }, + }; + cell.alignment = this.cloneStyle(templateCell?.alignment) || { vertical: 'middle', horizontal: 'center', wrapText: true }; + cell.border = this.cloneStyle(templateCell?.border) || this.getDefaultBorder(); + } + } + + private applyBodyStyle( + sheet: import('exceljs').Worksheet, + columns: TableExportColumn[], + rowCount: number, + templateStyle?: TemplateStyleSnapshot, + ): void { + for (let rowIndex = 2; rowIndex <= rowCount + 1; rowIndex += 1) { + const row = sheet.getRow(rowIndex); + const isEven = (rowIndex - 1) % 2 === 0; + const templateRowStyle = isEven + ? (templateStyle?.bodyAltStyle ?? templateStyle?.bodyStyle) + : (templateStyle?.bodyStyle ?? templateStyle?.bodyAltStyle); + + columns.forEach((column, columnIndex) => { + const cell = row.getCell(columnIndex + 1); + cell.font = this.cloneStyle(templateRowStyle?.font) || { name: 'Calibri', size: 11, color: { argb: 'FF1F2937' } }; + cell.border = this.cloneStyle(templateRowStyle?.border) || this.getDefaultBorder(); + cell.alignment = this.cloneStyle(templateRowStyle?.alignment) || this.getAlignment(column.type); + + if (templateRowStyle?.fill) { + const fill = this.cloneStyle(templateRowStyle.fill); + if (fill) cell.fill = fill; + } else if (isEven) { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFF7FAFF' }, + }; + } + + if (column.type === 'number') cell.numFmt = '#,##0.00'; + if (column.type === 'currency') cell.numFmt = '"R$" #,##0.00'; + if (column.type === 'date') cell.numFmt = 'dd/mm/yyyy'; + if (column.type === 'datetime') cell.numFmt = 'dd/mm/yyyy hh:mm'; + }); + } + } + + private applyColumnWidths( + sheet: import('exceljs').Worksheet, + columns: TableExportColumn[], + rows: T[], + templateStyle?: TemplateStyleSnapshot, + ): void { + columns.forEach((column, columnIndex) => { + if (column.width && column.width > 0) { + sheet.getColumn(columnIndex + 1).width = column.width; + return; + } + + const templateWidth = templateStyle?.columnWidths?.[columnIndex]; + if (templateWidth && templateWidth > 0) { + sheet.getColumn(columnIndex + 1).width = templateWidth; + return; + } + + const headerLength = (column.header ?? '').length; + let maxLength = headerLength; + + rows.forEach((row, rowIndex) => { + const value = column.value(row, rowIndex); + const printable = this.toPrintableValue(value, column.type); + if (printable.length > maxLength) maxLength = printable.length; + }); + + const target = Math.max(12, Math.min(maxLength + 3, 48)); + sheet.getColumn(columnIndex + 1).width = target; + }); + } + + private applyAutoFilter(sheet: import('exceljs').Worksheet, columnCount: number): void { + if (columnCount <= 0) return; + sheet.autoFilter = { + from: { row: 1, column: 1 }, + to: { row: 1, column: columnCount }, + }; + } + + private normalizeValue(value: unknown, type?: ExportCellType): string | number | Date | boolean | null { + if (value === null || value === undefined || value === '') return null; + + if (type === 'number' || type === 'currency') { + const numeric = this.toNumber(value); + return numeric ?? String(value); + } + + if (type === 'date' || type === 'datetime') { + const parsedDate = this.toDate(value); + return parsedDate ?? String(value); + } + + if (type === 'boolean') { + if (typeof value === 'boolean') return value; + return this.normalizeBoolean(value); + } + + return String(value); + } + + private toPrintableValue(value: unknown, type?: ExportCellType): string { + if (value === null || value === undefined || value === '') return ''; + + if (type === 'date' || type === 'datetime') { + const parsedDate = this.toDate(value); + if (!parsedDate) return String(value); + const datePart = `${this.pad2(parsedDate.getDate())}/${this.pad2(parsedDate.getMonth() + 1)}/${parsedDate.getFullYear()}`; + if (type === 'date') return datePart; + return `${datePart} ${this.pad2(parsedDate.getHours())}:${this.pad2(parsedDate.getMinutes())}`; + } + + if (type === 'number' || type === 'currency') { + const numeric = this.toNumber(value); + if (numeric === null) return String(value); + if (type === 'currency') { + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(numeric); + } + return new Intl.NumberFormat('pt-BR').format(numeric); + } + + if (type === 'boolean') { + return this.normalizeBoolean(value) ? 'Sim' : 'Nao'; + } + + return String(value); + } + + private toNumber(value: unknown): number | null { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : null; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) return null; + const normalized = trimmed + .replace(/[^\d,.-]/g, '') + .replace(/\.(?=\d{3}(\D|$))/g, '') + .replace(',', '.'); + const parsed = Number(normalized); + return Number.isFinite(parsed) ? parsed : null; + } + + return null; + } + + private toDate(value: unknown): Date | null { + if (value instanceof Date) { + return Number.isNaN(value.getTime()) ? null : value; + } + + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) return null; + + const brDate = trimmed.match(/^(\d{2})\/(\d{2})\/(\d{4})(?:\s+(\d{2}):(\d{2}))?$/); + if (brDate) { + const day = Number(brDate[1]); + const month = Number(brDate[2]) - 1; + const year = Number(brDate[3]); + const hour = Number(brDate[4] ?? '0'); + const minute = Number(brDate[5] ?? '0'); + const parsedBr = new Date(year, month, day, hour, minute); + return Number.isNaN(parsedBr.getTime()) ? null : parsedBr; + } + + const parsed = new Date(trimmed); + return Number.isNaN(parsed.getTime()) ? null : parsed; + } + + return null; + } + + private normalizeBoolean(value: unknown): boolean { + if (typeof value === 'boolean') return value; + const normalized = String(value ?? '') + .trim() + .toLowerCase(); + return normalized === 'true' || normalized === '1' || normalized === 'sim' || normalized === 'yes'; + } + + private ensureXlsxExtension(fileName: string): string { + const safe = (fileName ?? 'export').trim() || 'export'; + return safe.toLowerCase().endsWith('.xlsx') ? safe : `${safe}.xlsx`; + } + + private sanitizeSheetName(name: string): string { + const safe = (name ?? 'Dados').replace(/[\\/*?:[\]]/g, '').trim(); + return (safe || 'Dados').slice(0, 31); + } + + private shouldExcludeColumnByHeader(header: string | undefined): boolean { + const normalized = this.normalizeHeader(header); + if (!normalized) return false; + + const tokens = normalized.split(/[^a-z0-9]+/).filter(Boolean); + if (!tokens.length) return false; + + return tokens.includes('id') || tokens.includes('item'); + } + + private normalizeHeader(value: string | undefined): string { + return (value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim(); + } + + private downloadBuffer(buffer: ArrayBuffer, fileName: string): void { + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = fileName; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); + } + + private getAlignment(type?: ExportCellType): Partial { + if (type === 'number' || type === 'currency') { + return { vertical: 'middle', horizontal: 'right' }; + } + if (type === 'boolean') { + return { vertical: 'middle', horizontal: 'center' }; + } + return { vertical: 'middle', horizontal: 'left', wrapText: true }; + } + + private getDefaultBorder(): Partial { + return { + top: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + left: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + right: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + bottom: { style: 'thin', color: { argb: 'FFD6DCE8' } }, + }; + } + + private pad2(value: number): string { + return value.toString().padStart(2, '0'); + } + + private async extractTemplateStyle( + excelJsModule: typeof import('exceljs'), + templateBuffer: ArrayBuffer | null, + ): Promise { + if (!templateBuffer) return undefined; + + try { + const workbook = new excelJsModule.Workbook(); + await workbook.xlsx.load(templateBuffer); + const sheet = workbook.getWorksheet(1); + if (!sheet) return undefined; + + const headerRow = sheet.getRow(1); + const headerCount = Math.max(headerRow.actualCellCount, 1); + const headerStyles: CellStyleSnapshot[] = []; + for (let col = 1; col <= headerCount; col += 1) { + headerStyles.push(this.captureCellStyle(headerRow.getCell(col))); + } + + const bodyStyle = this.captureFirstStyledCellRow(sheet.getRow(2)); + const bodyAltStyle = this.captureFirstStyledCellRow(sheet.getRow(3)); + const columnWidths = (sheet.columns ?? []).map((column) => column.width); + + return { headerStyles, bodyStyle, bodyAltStyle, columnWidths }; + } catch { + return undefined; + } + } + + private async resolveTemplateStyle( + excelJsModule: typeof import('exceljs'), + templateBuffer: ArrayBuffer | null, + ): Promise { + if (templateBuffer) { + const style = await this.extractTemplateStyle(excelJsModule, templateBuffer); + if (style) this.cachedDefaultTemplateStyle = style; + return style; + } + + return this.cachedDefaultTemplateStyle; + } + + private async getDefaultTemplateBuffer(): Promise { + if (this.defaultTemplateBufferPromise) { + return this.defaultTemplateBufferPromise; + } + + this.defaultTemplateBufferPromise = this.fetchDefaultTemplateBuffer(); + const buffer = await this.defaultTemplateBufferPromise; + if (!buffer) this.defaultTemplateBufferPromise = null; + return buffer; + } + + private async fetchDefaultTemplateBuffer(): Promise { + try { + const params = new HttpParams().set('_', `${Date.now()}`); + const blob = await firstValueFrom( + this.http.get(`${this.templatesApiBase}/planilha-geral`, { + params, + responseType: 'blob', + }) + ); + return await blob.arrayBuffer(); + } catch { + return null; + } + } + + private captureFirstStyledCellRow(row: import('exceljs').Row): CellStyleSnapshot | undefined { + if (!row) return undefined; + const cellCount = Math.max(row.actualCellCount, 1); + for (let col = 1; col <= cellCount; col += 1) { + const captured = this.captureCellStyle(row.getCell(col)); + if (captured.font || captured.fill || captured.border || captured.alignment) { + return captured; + } + } + return undefined; + } + + private captureCellStyle(cell: import('exceljs').Cell): CellStyleSnapshot { + return { + font: this.cloneStyle(cell.font), + fill: this.cloneStyle(cell.fill), + border: this.cloneStyle(cell.border), + alignment: this.cloneStyle(cell.alignment), + }; + } + + private getTemplateStyleByIndex(style: TemplateStyleSnapshot | undefined, index: number): CellStyleSnapshot | undefined { + if (!style || !style.headerStyles.length) return undefined; + return style.headerStyles[index] ?? style.headerStyles[style.headerStyles.length - 1]; + } + + private cloneStyle(value: T | undefined): T | undefined { + if (!value) return undefined; + try { + return JSON.parse(JSON.stringify(value)) as T; + } catch { + return value; + } + } +}