diff --git a/package-lock.json b/package-lock.json index bcb1605..c495cf0 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" @@ -609,13 +610,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -624,9 +625,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -639,7 +640,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -683,14 +683,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -713,13 +713,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -750,29 +750,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -825,27 +825,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -855,33 +855,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -889,9 +889,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1354,10 +1354,64 @@ "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/@gar/promise-retry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", + "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", "dev": true, "license": "MIT", "engines": { @@ -1718,29 +1772,6 @@ } } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2390,9 +2421,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2413,18 +2444,18 @@ } }, "node_modules/@npmcli/git": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", - "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/promise-spawn": "^9.0.0", "ini": "^6.0.0", "lru-cache": "^11.2.1", "npm-pick-manifest": "^11.0.1", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^6.0.0" }, @@ -2443,19 +2474,19 @@ } }, "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2473,13 +2504,13 @@ } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -2516,9 +2547,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", - "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2528,41 +2559,64 @@ "json-parse-even-better-errors": "^5.0.0", "proc-log": "^6.0.0", "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" + "spdx-expression-parse": "^4.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@npmcli/package-json/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2592,23 +2646,23 @@ } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -2628,9 +2682,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", - "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", "dev": true, "license": "ISC", "dependencies": { @@ -2638,23 +2692,12 @@ "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" + "proc-log": "^6.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/@npmcli/run-script/node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -2665,35 +2708,19 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -2703,25 +2730,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -2740,9 +2767,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -2761,9 +2788,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -2782,9 +2809,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -2803,9 +2830,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -2824,9 +2851,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -2845,9 +2872,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -2866,9 +2893,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -2887,9 +2914,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -2908,9 +2935,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -2929,9 +2956,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -2950,9 +2977,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -2971,9 +2998,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -2991,20 +3018,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/@parcel/watcher/node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -3470,17 +3483,40 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@tufjs/models/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3514,16 +3550,16 @@ "license": "MIT" }, "node_modules/@types/jasmine": { - "version": "5.1.13", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", - "integrity": "sha512-MYCcDkruFc92LeYZux5BC0dmqo2jk+M5UIZ4/oFnAPCXN9mCcQhLyj7F3/Za7rocVyt5YRr1MmqJqFlvQ9LVcg==", + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz", + "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", - "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "dev": true, "license": "MIT", "peer": true, @@ -3647,9 +3683,9 @@ } }, "node_modules/ansi-escapes": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", - "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -3715,11 +3751,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 +3895,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,10 +3930,27 @@ "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", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { @@ -3791,7 +3960,7 @@ "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", + "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" }, @@ -3849,7 +4018,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", @@ -3870,9 +4038,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -3891,11 +4059,11 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -3904,6 +4072,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 +4112,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", @@ -3944,28 +4162,51 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/cacache/node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3973,16 +4214,16 @@ } }, "node_modules/cacache/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4020,9 +4261,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "dev": true, "funding": [ { @@ -4040,6 +4281,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 +4467,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,10 +4624,16 @@ "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", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -4369,6 +4642,35 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": { @@ -4433,6 +4735,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 +4885,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", @@ -4585,9 +4932,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.262", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", - "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "dev": true, "license": "ISC" }, @@ -4608,35 +4955,19 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, + "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", - "optional": true, "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "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", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", "dev": true, "license": "MIT", "dependencies": { @@ -4646,9 +4977,9 @@ "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.18.3" }, "engines": { "node": ">=10.2.0" @@ -4678,24 +5009,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/engine.io/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4920,6 +5233,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", @@ -4973,13 +5306,13 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -4998,6 +5331,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", @@ -5054,9 +5400,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -5068,13 +5414,17 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "dev": true, "license": "ISC" }, @@ -5119,6 +5469,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 +5507,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 +5524,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", @@ -5200,9 +5584,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true, "license": "MIT", "engines": { @@ -5255,8 +5639,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -5310,7 +5693,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": { @@ -5366,9 +5748,9 @@ } }, "node_modules/hono": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", - "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", + "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", "dev": true, "license": "MIT", "peer": true, @@ -5390,9 +5772,9 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5407,9 +5789,9 @@ "license": "MIT" }, "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -5422,14 +5804,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5511,9 +5893,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -5527,6 +5909,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", @@ -5540,26 +5942,55 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/ignore-walk/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "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", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -5578,7 +6009,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 +6019,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": { @@ -5603,9 +6032,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "dev": true, "license": "MIT", "engines": { @@ -5749,6 +6178,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", @@ -5859,9 +6294,9 @@ "peer": true }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", + "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", "dev": true, "license": "MIT", "funding": { @@ -5952,6 +6387,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,13 +6881,69 @@ "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", "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -6424,9 +6957,9 @@ } }, "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, @@ -6483,6 +7016,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", @@ -6638,12 +7250,13 @@ } }, "node_modules/make-fetch-happen": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", - "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", @@ -6653,7 +7266,6 @@ "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "ssri": "^13.0.0" }, "engines": { @@ -6703,35 +7315,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -6786,10 +7369,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6802,18 +7384,17 @@ "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" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -6832,9 +7413,9 @@ } }, "node_modules/minipass-fetch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", - "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6846,7 +7427,7 @@ "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { - "encoding": "^0.1.13" + "iconv-lite": "^0.7.2" } }, "node_modules/minipass-flush": { @@ -6945,7 +7526,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" @@ -6972,9 +7552,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", + "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", "dev": true, "license": "MIT", "optional": true, @@ -7094,13 +7674,13 @@ } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/proc-log": { @@ -7114,13 +7694,13 @@ } }, "node_modules/node-gyp/node_modules/which": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", - "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" @@ -7130,9 +7710,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -7156,7 +7736,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" @@ -7215,9 +7794,9 @@ } }, "node_modules/npm-packlist": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", - "integrity": "sha512-zPukTwJMOu5X5uvm0fztwS5Zxyvmk38H/LfidkOMt3gbZVCyro2cD/ETzwzVPcWZA3JOyPznfUN/nkyFiyUbxg==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", "dev": true, "license": "ISC", "dependencies": { @@ -7337,7 +7916,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" @@ -7384,9 +7962,9 @@ } }, "node_modules/ordered-binary": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz", - "integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", "dev": true, "license": "MIT", "optional": true @@ -7446,6 +8024,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 +8111,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" @@ -7551,9 +8134,9 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7561,16 +8144,16 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7632,9 +8215,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -7677,6 +8260,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", @@ -7691,6 +8280,16 @@ "node": ">=10" } }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7723,9 +8322,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7764,6 +8363,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", @@ -7851,9 +8494,9 @@ } }, "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", "engines": { @@ -7953,6 +8596,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 +8663,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", @@ -8014,32 +8689,36 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -8050,8 +8729,18 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, + "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", @@ -8218,16 +8907,16 @@ } }, "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", - "debug": "~4.3.2", + "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" @@ -8237,66 +8926,30 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "debug": "~4.4.1", + "ws": "~8.18.3" } }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io/node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -8311,24 +8964,6 @@ "node": ">= 0.6" } }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8433,17 +9068,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/spdx-exceptions": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", @@ -8452,9 +9076,9 @@ "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8463,16 +9087,16 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", - "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/ssri": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", - "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", "dev": true, "license": "ISC", "dependencies": { @@ -8520,6 +9144,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", @@ -8539,13 +9172,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -8581,9 +9214,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.10.tgz", + "integrity": "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8597,6 +9230,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 +9277,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 +9305,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,10 +9446,58 @@ "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", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -8820,6 +9525,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,15 +9541,13 @@ "node": ">= 0.4.0" } }, - "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", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.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-name": { @@ -8867,7 +9576,6 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -9092,13 +9800,12 @@ "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": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { @@ -9117,6 +9824,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 +9888,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/app.routes.ts b/src/app/app.routes.ts index 0b219bf..6bb72d7 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -9,6 +9,7 @@ import { Faturamento } from './pages/faturamento/faturamento'; import { authGuard } from './guards/auth.guard'; import { sysadminOrGestorGuard } from './guards/sysadmin-or-gestor.guard'; +import { sysadminOrFinanceiroGuard } from './guards/sysadmin-or-financeiro.guard'; import { sysadminOnlyGuard } from './guards/sysadmin-only.guard'; import { DadosUsuarios } from './pages/dados-usuarios/dados-usuarios'; import { VigenciaComponent } from './pages/vigencia/vigencia'; @@ -19,6 +20,7 @@ import { ChipsControleRecebidos } from './pages/chips-controle-recebidos/chips-c import { Resumo } from './pages/resumo/resumo'; import { Parcelamentos } from './pages/parcelamentos/parcelamentos'; import { Historico } from './pages/historico/historico'; +import { HistoricoLinhas } from './pages/historico-linhas/historico-linhas'; import { Perfil } from './pages/perfil/perfil'; import { SystemProvisionUserPage } from './pages/system-provision-user/system-provision-user'; import { SolicitacoesLinhas } from './pages/solicitacoes-linhas/solicitacoes-linhas'; @@ -30,15 +32,16 @@ export const routes: Routes = [ { path: 'geral', component: Geral, canActivate: [authGuard], title: 'Geral' }, { path: 'mureg', component: Mureg, canActivate: [authGuard], title: 'Mureg' }, - { path: 'faturamento', component: Faturamento, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Faturamento' }, + { path: 'faturamento', component: Faturamento, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Faturamento' }, { path: 'dadosusuarios', component: DadosUsuarios, canActivate: [authGuard], title: 'Dados dos Usuários' }, { path: 'vigencia', component: VigenciaComponent, canActivate: [authGuard], title: 'Vigência' }, { path: 'trocanumero', component: TrocaNumero, canActivate: [authGuard], title: 'Troca de Número' }, { path: 'notificacoes', component: Notificacoes, canActivate: [authGuard], title: 'Notificações' }, { path: 'chips-controle-recebidos', component: ChipsControleRecebidos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Chips Controle Recebidos' }, { path: 'resumo', component: Resumo, canActivate: [authGuard], title: 'Resumo' }, - { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Parcelamentos' }, + { path: 'parcelamentos', component: Parcelamentos, canActivate: [authGuard, sysadminOrFinanceiroGuard], title: 'Parcelamentos' }, { path: 'historico', component: Historico, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico' }, + { path: 'historico-linhas', component: HistoricoLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Histórico de Linhas' }, { path: 'solicitacoes', component: SolicitacoesLinhas, canActivate: [authGuard, sysadminOrGestorGuard], title: 'Solicitações' }, { path: 'perfil', component: Perfil, canActivate: [authGuard], title: 'Perfil' }, { diff --git a/src/app/app.ts b/src/app/app.ts index 0d6c4dc..5a733b1 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -41,6 +41,7 @@ export class AppComponent { '/resumo', '/parcelamentos', '/historico', + '/historico-linhas', '/perfil', '/system', ]; diff --git a/src/app/components/header/header.html b/src/app/components/header/header.html index 4c1289c..8df4721 100644 --- a/src/app/components/header/header.html +++ b/src/app/components/header/header.html @@ -539,15 +539,18 @@ Mureg - + Faturamento - + Parcelamentos Histórico + + Histórico de Linhas + Solicitações diff --git a/src/app/components/header/header.ts b/src/app/components/header/header.ts index ef45da7..d18f6fe 100644 --- a/src/app/components/header/header.ts +++ b/src/app/components/header/header.ts @@ -13,6 +13,7 @@ import { CustomSelectComponent } from '../custom-select/custom-select'; import { Subscription } from 'rxjs'; import { environment } from '../../../environments/environment'; import { confirmActionModal, confirmDeletionWithTyping, showDeletionWarning } from '../../utils/destructive-confirmation'; +import { buildApiBaseUrl } from '../../utils/api-base.util'; @Component({ selector: 'app-header', @@ -34,7 +35,10 @@ export class Header implements AfterViewInit, OnDestroy { isLoggedHeader = false; isHome = false; isSysAdmin = false; + isGestor = false; + isFinanceiro = false; canViewAll = false; + canViewFinancialPages = false; clientTenantDisplayName = ''; private clientTenantNameTenantId: string | null = null; private readonly baseApi: string; @@ -58,10 +62,11 @@ export class Header implements AfterViewInit, OnDestroy { createUserForbidden = false; createUserSuccess = ''; readonly permissionOptions = [ - { value: 'sysadmin', label: 'SysAdmin' }, - { value: 'gestor', label: 'Gestor' }, - { value: 'cliente', label: 'Cliente' }, - ]; + { value: 'sysadmin', label: 'SysAdmin' }, + { value: 'gestor', label: 'Gestor' }, + { value: 'financeiro', label: 'Financeiro' }, + { value: 'cliente', label: 'Cliente' }, +]; manageUsersLoading = false; manageUsersErrors: ApiFieldError[] = []; @@ -93,6 +98,7 @@ export class Header implements AfterViewInit, OnDestroy { '/resumo', '/parcelamentos', '/historico', + '/historico-linhas', '/solicitacoes', '/perfil', '/system', @@ -108,8 +114,7 @@ export class Header implements AfterViewInit, OnDestroy { private hostElement: ElementRef, @Inject(PLATFORM_ID) private platformId: object ) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); this.createUserForm = this.fb.group( { @@ -214,15 +219,22 @@ export class Header implements AfterViewInit, OnDestroy { private syncPermissions() { if (!isPlatformBrowser(this.platformId)) { this.isSysAdmin = false; + this.isGestor = false; + this.isFinanceiro = false; this.canViewAll = false; + this.canViewFinancialPages = false; this.clientTenantDisplayName = ''; this.clientTenantNameTenantId = null; return; } const isSysAdmin = this.authService.hasRole('sysadmin'); const isGestor = this.authService.hasRole('gestor'); + const isFinanceiro = this.authService.hasRole('financeiro'); this.isSysAdmin = isSysAdmin; - this.canViewAll = isSysAdmin || isGestor; + this.isGestor = isGestor; + this.isFinanceiro = isFinanceiro; + this.canViewAll = isSysAdmin || isGestor || isFinanceiro; + this.canViewFinancialPages = isSysAdmin || isFinanceiro; if (!this.isClientHeader) { this.clientTenantDisplayName = ''; @@ -498,7 +510,10 @@ export class Header implements AfterViewInit, OnDestroy { this.optionsOpen = false; this.notificationsOpen = false; this.isSysAdmin = false; + this.isGestor = false; + this.isFinanceiro = false; this.canViewAll = false; + this.canViewFinancialPages = false; this.router.navigate(['/']); } diff --git a/src/app/components/modal-layer/modal-layer.html b/src/app/components/modal-layer/modal-layer.html new file mode 100644 index 0000000..0e62b4d --- /dev/null +++ b/src/app/components/modal-layer/modal-layer.html @@ -0,0 +1,14 @@ + +
+ +
+ +
+
diff --git a/src/app/components/modal-layer/modal-layer.scss b/src/app/components/modal-layer/modal-layer.scss new file mode 100644 index 0000000..2dad415 --- /dev/null +++ b/src/app/components/modal-layer/modal-layer.scss @@ -0,0 +1,35 @@ +.modal-backdrop-custom { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 9990; + backdrop-filter: blur(4px); +} + +.modal-custom { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 9995; + padding: 16px; +} + +.lg-modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 9995; + padding: 16px; +} + +.lg-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 9990; + backdrop-filter: blur(2px); +} diff --git a/src/app/components/modal-layer/modal-layer.ts b/src/app/components/modal-layer/modal-layer.ts new file mode 100644 index 0000000..264f041 --- /dev/null +++ b/src/app/components/modal-layer/modal-layer.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'app-modal-layer', + standalone: true, + imports: [CommonModule], + templateUrl: './modal-layer.html', + styleUrls: ['./modal-layer.scss'], +}) +export class ModalLayerComponent { + @Input() open = false; + @Input() showBackdrop = true; + @Input() closeOnOverlay = true; + @Input() backdropClass = 'modal-backdrop-custom'; + @Input() overlayClass = 'modal-custom'; + + @Output() close = new EventEmitter(); + + onOverlayClick(): void { + if (!this.closeOnOverlay) return; + this.close.emit(); + } +} diff --git a/src/app/components/page-modals/chips-controle-modals/chips-controle-modals.html b/src/app/components/page-modals/chips-controle-modals/chips-controle-modals.html new file mode 100644 index 0000000..99a893d --- /dev/null +++ b/src/app/components/page-modals/chips-controle-modals/chips-controle-modals.html @@ -0,0 +1,370 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/components/page-modals/chips-controle-modals/chips-controle-modals.ts b/src/app/components/page-modals/chips-controle-modals/chips-controle-modals.ts new file mode 100644 index 0000000..f1392b0 --- /dev/null +++ b/src/app/components/page-modals/chips-controle-modals/chips-controle-modals.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-chips-controle-modals', + standalone: true, + imports: [CommonModule, FormsModule, ModalLayerComponent], + templateUrl: './chips-controle-modals.html', + styleUrls: ['../../../pages/chips-controle-recebidos/chips-controle-recebidos.scss'], +}) +export class ChipsControleModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/components/page-modals/dados-usuarios-modals/dados-usuarios-modals.html b/src/app/components/page-modals/dados-usuarios-modals/dados-usuarios-modals.html new file mode 100644 index 0000000..11537d5 --- /dev/null +++ b/src/app/components/page-modals/dados-usuarios-modals/dados-usuarios-modals.html @@ -0,0 +1,277 @@ + + + + + + + + + + + + + diff --git a/src/app/components/page-modals/dados-usuarios-modals/dados-usuarios-modals.ts b/src/app/components/page-modals/dados-usuarios-modals/dados-usuarios-modals.ts new file mode 100644 index 0000000..1fc5514 --- /dev/null +++ b/src/app/components/page-modals/dados-usuarios-modals/dados-usuarios-modals.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../custom-select/custom-select'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-dados-usuarios-modals', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent], + templateUrl: './dados-usuarios-modals.html', + styleUrls: ['../../../pages/dados-usuarios/dados-usuarios.scss'], +}) +export class DadosUsuariosModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/components/page-modals/faturamento-modals/faturamento-modals.html b/src/app/components/page-modals/faturamento-modals/faturamento-modals.html new file mode 100644 index 0000000..fbb28e9 --- /dev/null +++ b/src/app/components/page-modals/faturamento-modals/faturamento-modals.html @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + diff --git a/src/app/components/page-modals/faturamento-modals/faturamento-modals.ts b/src/app/components/page-modals/faturamento-modals/faturamento-modals.ts new file mode 100644 index 0000000..34a32bc --- /dev/null +++ b/src/app/components/page-modals/faturamento-modals/faturamento-modals.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-faturamento-modals', + standalone: true, + imports: [CommonModule, FormsModule, ModalLayerComponent], + templateUrl: './faturamento-modals.html', + styleUrls: ['../../../pages/faturamento/faturamento.scss'], +}) +export class FaturamentoModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/components/page-modals/geral-modals/geral-modals.html b/src/app/components/page-modals/geral-modals/geral-modals.html new file mode 100644 index 0000000..afbae88 --- /dev/null +++ b/src/app/components/page-modals/geral-modals/geral-modals.html @@ -0,0 +1,1743 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/components/page-modals/geral-modals/geral-modals.ts b/src/app/components/page-modals/geral-modals/geral-modals.ts new file mode 100644 index 0000000..cdc4e0e --- /dev/null +++ b/src/app/components/page-modals/geral-modals/geral-modals.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../custom-select/custom-select'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-geral-modals', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent], + templateUrl: './geral-modals.html', + styleUrls: ['../../../pages/geral/geral.scss'], +}) +export class GeralModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/components/page-modals/mureg-modals/mureg-modals.html b/src/app/components/page-modals/mureg-modals/mureg-modals.html new file mode 100644 index 0000000..f4a2a7f --- /dev/null +++ b/src/app/components/page-modals/mureg-modals/mureg-modals.html @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/components/page-modals/mureg-modals/mureg-modals.ts b/src/app/components/page-modals/mureg-modals/mureg-modals.ts new file mode 100644 index 0000000..e0f300d --- /dev/null +++ b/src/app/components/page-modals/mureg-modals/mureg-modals.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../custom-select/custom-select'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-mureg-modals', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent], + templateUrl: './mureg-modals.html', + styleUrls: ['../../../pages/mureg/mureg.scss'], +}) +export class MuregModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.html b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.html similarity index 96% rename from src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.html rename to src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.html index fd6c79b..1291946 100644 --- a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.html +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.html @@ -1,6 +1,10 @@ -
-
-
+ +
-
+
diff --git a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.scss b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.scss similarity index 94% rename from src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.scss rename to src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.scss index f66ede4..71a1ee0 100644 --- a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.scss +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.scss @@ -5,26 +5,6 @@ --focus-ring: var(--pg-focus-ring, 0 0 0 3px rgba(31, 79, 214, 0.22)); } -.lg-backdrop { - position: fixed; - inset: 0; - background: - radial-gradient(circle at 15% 0%, rgba(31, 79, 214, 0.15), rgba(15, 23, 42, 0.66) 42%), - rgba(15, 23, 42, 0.58); - z-index: 9990; - backdrop-filter: blur(4px); -} - -.lg-modal { - position: fixed; - inset: 0; - z-index: 9995; - display: flex; - align-items: center; - justify-content: center; - padding: 16px; -} - .lg-modal-card { width: min(1040px, 96vw); max-height: 92vh; diff --git a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.ts similarity index 96% rename from src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts rename to src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.ts index ab003b4..35dfff9 100644 --- a/src/app/pages/parcelamentos/components/parcelamento-create-modal/parcelamento-create-modal.ts +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal.ts @@ -1,7 +1,8 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { CustomSelectComponent } from '../../../../components/custom-select/custom-select'; +import { CustomSelectComponent } from '../../../../custom-select/custom-select'; +import { ModalLayerComponent } from '../../../../modal-layer/modal-layer'; export type MonthOption = { value: number; label: string }; @@ -31,7 +32,7 @@ type PreviewRow = { @Component({ selector: 'app-parcelamento-create-modal', standalone: true, - imports: [CommonModule, FormsModule, CustomSelectComponent], + imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent], templateUrl: './parcelamento-create-modal.html', styleUrls: ['./parcelamento-create-modal.scss'], }) diff --git a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html similarity index 90% rename from src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html rename to src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html index 1f152f3..012ad1b 100644 --- a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.html @@ -1,6 +1,10 @@ -
-
-
+ +
-
+
diff --git a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss similarity index 91% rename from src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss rename to src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss index 8b610ab..99e5918 100644 --- a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.scss @@ -4,24 +4,6 @@ --focus-ring: 0 0 0 3px rgba(227, 61, 207, 0.16); } -.lg-backdrop { - position: fixed; - inset: 0; - background: radial-gradient(circle at 20% 0%, rgba(227, 61, 207, 0.2), rgba(0, 0, 0, 0.56) 42%); - z-index: 9990; - backdrop-filter: blur(5px); -} - -.lg-modal { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 9995; - padding: 16px; -} - .lg-modal-card { background: #ffffff; border: 1px solid rgba(255, 255, 255, 0.88); diff --git a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts similarity index 88% rename from src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts rename to src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts index 99a923f..5504042 100644 --- a/src/app/pages/parcelamentos/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamento-detalhamento-anual-modal/parcelamento-detalhamento-anual-modal.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import { ModalLayerComponent } from '../../../../modal-layer/modal-layer'; export type AnnualMonthValue = { month: number; @@ -20,7 +21,7 @@ export type AnnualRow = { @Component({ selector: 'app-parcelamento-detalhamento-anual-modal', standalone: true, - imports: [CommonModule, FormsModule], + imports: [CommonModule, FormsModule, ModalLayerComponent], templateUrl: './parcelamento-detalhamento-anual-modal.html', styleUrls: ['./parcelamento-detalhamento-anual-modal.scss'], }) diff --git a/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.html b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters.html similarity index 100% rename from src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.html rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters.html diff --git a/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.scss b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters.scss similarity index 100% rename from src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.scss rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters.scss diff --git a/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.ts b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters.ts similarity index 92% rename from src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.ts rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters.ts index 546a7bd..bb47c41 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-filters/parcelamentos-filters.ts +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { CustomSelectComponent } from '../../../../components/custom-select/custom-select'; +import { CustomSelectComponent } from '../../../../custom-select/custom-select'; export type MonthOption = { value: number; label: string }; diff --git a/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.html b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis.html similarity index 100% rename from src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.html rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis.html diff --git a/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.scss b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis.scss similarity index 100% rename from src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.scss rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis.scss diff --git a/src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.ts b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis.ts similarity index 100% rename from src/app/pages/parcelamentos/components/parcelamentos-kpis/parcelamentos-kpis.ts rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis.ts diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.html similarity index 98% rename from src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.html index fb8c03c..3b3c311 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.html +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.html @@ -100,6 +100,7 @@ diff --git a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.scss similarity index 98% rename from src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.scss index f1c8de2..cd072b7 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.scss +++ b/src/app/components/page-modals/parcelamento-modals/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/components/parcelamentos-table/parcelamentos-table.ts b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.ts similarity index 93% rename from src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts rename to src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.ts index 45a2629..5aa6e27 100644 --- a/src/app/pages/parcelamentos/components/parcelamentos-table/parcelamentos-table.ts +++ b/src/app/components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { ParcelamentoListItem } from '../../../../services/parcelamentos.service'; +import { ParcelamentoListItem } from '../../../../../services/parcelamentos.service'; export type ParcelamentoSegment = 'todos' | 'ativos' | 'futuros' | 'finalizados'; @@ -26,7 +26,8 @@ export class ParcelamentosTableComponent { @Input() items: ParcelamentoViewItem[] = []; @Input() loading = false; @Input() errorMessage = ''; - @Input() isSysAdmin = false; + @Input() canEdit = false; + @Input() canDelete = false; @Input() segment: ParcelamentoSegment = 'todos'; @Input() segmentCounts: Record = { diff --git a/src/app/components/page-modals/parcelamento-modals/parcelamentos-modals.html b/src/app/components/page-modals/parcelamento-modals/parcelamentos-modals.html new file mode 100644 index 0000000..a943780 --- /dev/null +++ b/src/app/components/page-modals/parcelamento-modals/parcelamentos-modals.html @@ -0,0 +1,180 @@ + + +
+ + + + + +
+
+ + + + + + + + + + + diff --git a/src/app/components/page-modals/parcelamento-modals/parcelamentos-modals.ts b/src/app/components/page-modals/parcelamento-modals/parcelamentos-modals.ts new file mode 100644 index 0000000..282d08a --- /dev/null +++ b/src/app/components/page-modals/parcelamento-modals/parcelamentos-modals.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { ParcelamentoCreateModalComponent } from './components/parcelamento-create-modal/parcelamento-create-modal'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-parcelamentos-modals', + standalone: true, + imports: [CommonModule, ModalLayerComponent, ParcelamentoCreateModalComponent], + templateUrl: './parcelamentos-modals.html', + styleUrls: ['../../../pages/parcelamentos/parcelamentos.scss'], +}) +export class ParcelamentosModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/components/page-modals/troca-numero-modals/troca-numero-modals.html b/src/app/components/page-modals/troca-numero-modals/troca-numero-modals.html new file mode 100644 index 0000000..6a6d683 --- /dev/null +++ b/src/app/components/page-modals/troca-numero-modals/troca-numero-modals.html @@ -0,0 +1,181 @@ + + + + + + + + + + diff --git a/src/app/components/page-modals/troca-numero-modals/troca-numero-modals.ts b/src/app/components/page-modals/troca-numero-modals/troca-numero-modals.ts new file mode 100644 index 0000000..909ab2f --- /dev/null +++ b/src/app/components/page-modals/troca-numero-modals/troca-numero-modals.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../custom-select/custom-select'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-troca-numero-modals', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent], + templateUrl: './troca-numero-modals.html', + styleUrls: ['../../../pages/troca-numero/troca-numero.scss'], +}) +export class TrocaNumeroModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/components/page-modals/vigencia-modals/vigencia-modals.html b/src/app/components/page-modals/vigencia-modals/vigencia-modals.html new file mode 100644 index 0000000..68c94c4 --- /dev/null +++ b/src/app/components/page-modals/vigencia-modals/vigencia-modals.html @@ -0,0 +1,262 @@ + + + + + + + + + + + + + diff --git a/src/app/components/page-modals/vigencia-modals/vigencia-modals.ts b/src/app/components/page-modals/vigencia-modals/vigencia-modals.ts new file mode 100644 index 0000000..e72798e --- /dev/null +++ b/src/app/components/page-modals/vigencia-modals/vigencia-modals.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { CustomSelectComponent } from '../../custom-select/custom-select'; +import { ModalLayerComponent } from '../../modal-layer/modal-layer'; +import { VmProxyHost } from '../vm-proxy-host'; + +@Component({ + selector: 'app-vigencia-modals', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent, ModalLayerComponent], + templateUrl: './vigencia-modals.html', + styleUrls: ['../../../pages/vigencia/vigencia.scss'], +}) +export class VigenciaModalsComponent extends VmProxyHost { + @Input() set vm(value: any) { + this.attachVm(value); + } +} diff --git a/src/app/components/page-modals/vm-proxy-host.ts b/src/app/components/page-modals/vm-proxy-host.ts new file mode 100644 index 0000000..f6b0276 --- /dev/null +++ b/src/app/components/page-modals/vm-proxy-host.ts @@ -0,0 +1,52 @@ +export abstract class VmProxyHost { + [key: string]: any; + + private __boundKeys = new Set(); + + protected attachVm(vm: any): void { + if (!vm) return; + + // Bind own enumerable fields so template reads/writes keep source state in sync. + Object.keys(vm).forEach((key) => this.bindField(vm, key)); + + let proto: any = Object.getPrototypeOf(vm); + while (proto && proto !== Object.prototype) { + for (const key of Object.getOwnPropertyNames(proto)) { + if (key === 'constructor' || key.startsWith('ng')) continue; + + const descriptor = Object.getOwnPropertyDescriptor(proto, key); + if (!descriptor) continue; + + if (descriptor.get || descriptor.set) { + this.bindField(vm, key); + continue; + } + + const value = (vm as any)[key]; + if (typeof value === 'function' && !this.__boundKeys.has(key)) { + (this as any)[key] = (...args: any[]) => value.apply(vm, args); + this.__boundKeys.add(key); + } + } + + proto = Object.getPrototypeOf(proto); + } + } + + private bindField(vm: any, key: string): void { + if (this.__boundKeys.has(key)) return; + + if (Object.prototype.hasOwnProperty.call(this, key)) return; + + Object.defineProperty(this, key, { + configurable: true, + enumerable: true, + get: () => (vm as any)[key], + set: (value: unknown) => { + (vm as any)[key] = value; + }, + }); + + this.__boundKeys.add(key); + } +} diff --git a/src/app/guards/sysadmin-or-financeiro.guard.ts b/src/app/guards/sysadmin-or-financeiro.guard.ts new file mode 100644 index 0000000..8ce5e8b --- /dev/null +++ b/src/app/guards/sysadmin-or-financeiro.guard.ts @@ -0,0 +1,27 @@ +import { inject, PLATFORM_ID } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { isPlatformBrowser } from '@angular/common'; +import { AuthService } from '../services/auth.service'; + +export const sysadminOrFinanceiroGuard: CanActivateFn = () => { + const router = inject(Router); + const platformId = inject(PLATFORM_ID); + const authService = inject(AuthService); + + if (!isPlatformBrowser(platformId)) { + // Em SSR não há storage do usuário para validar sessão/perfil. + return true; + } + + const token = authService.token; + if (!token) { + return router.parseUrl('/login'); + } + + const hasAccess = authService.hasRole('sysadmin') || authService.hasRole('financeiro'); + if (!hasAccess) { + return router.parseUrl('/dashboard'); + } + + return true; +}; diff --git a/src/app/guards/sysadmin-or-gestor.guard.ts b/src/app/guards/sysadmin-or-gestor.guard.ts index 252bae1..61b19a5 100644 --- a/src/app/guards/sysadmin-or-gestor.guard.ts +++ b/src/app/guards/sysadmin-or-gestor.guard.ts @@ -18,7 +18,10 @@ export const sysadminOrGestorGuard: CanActivateFn = () => { return router.parseUrl('/login'); } - const hasAccess = authService.hasRole('sysadmin') || authService.hasRole('gestor'); + const hasAccess = + authService.hasRole('sysadmin') || + authService.hasRole('gestor') || + authService.hasRole('financeiro'); if (!hasAccess) { return router.parseUrl('/dashboard'); } 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..7e18bfd 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 @@
+ -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss index 3c33e6f..8e59abe 100644 --- a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.scss @@ -616,8 +616,6 @@ /* ========================================================== */ /* MODAIS (mantidos) */ /* ========================================================== */ -.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } -.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } .modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; min-height: 0; } .modal-card.modal-xl-custom { width: min(980px, 92vw); max-height: 82vh; } .modal-card.modal-lg { width: min(720px, 92vw); max-height: 80vh; } diff --git a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts index 2885265..bd38697 100644 --- a/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts +++ b/src/app/pages/chips-controle-recebidos/chips-controle-recebidos.ts @@ -4,8 +4,18 @@ import { FormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { ChipsControleService, ChipVirgemListDto, ControleRecebidoListDto, SortDir, UpdateChipVirgemRequest, UpdateControleRecebidoRequest, CreateChipVirgemRequest, CreateControleRecebidoRequest } from '../../services/chips-controle.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { ChipsControleModalsComponent } from '../../components/page-modals/chips-controle-modals/chips-controle-modals'; import { AuthService } from '../../services/auth.service'; +import { TableExportService } from '../../services/table-export.service'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; +import { firstValueFrom } from 'rxjs'; // Interface para o Agrupamento interface ChipGroup { @@ -35,11 +45,12 @@ type ControleSortKey = @Component({ selector: 'app-chips-controle-recebidos', standalone: true, - imports: [CommonModule, FormsModule, CustomSelectComponent], + imports: [CommonModule, FormsModule, CustomSelectComponent, ChipsControleModalsComponent], templateUrl: './chips-controle-recebidos.html', styleUrls: ['./chips-controle-recebidos.scss'] }) export class ChipsControleRecebidos implements OnInit, OnDestroy { + readonly vm = this; activeTab: 'chips' | 'controle' = 'chips'; // --- Chips --- @@ -86,6 +97,7 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy { toastOpen = false; toastMessage = ''; toastType: 'success' | 'danger' = 'success'; + exporting = false; private toastTimer: any = null; chipDetailOpen = false; @@ -124,7 +136,8 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy { @Inject(PLATFORM_ID) private platformId: object, private service: ChipsControleService, private http: HttpClient, - private authService: AuthService + private authService: AuthService, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -417,6 +430,129 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy { this.fetchControle(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + if (this.activeTab === 'chips') { + const baseRows = [...(this.chipsRows ?? [])].sort((a, b) => (a.item ?? 0) - (b.item ?? 0)); + const rows = await this.fetchDetailedChipRowsForExport(baseRows); + if (!rows.length) { + this.showToast('Nenhum registro encontrado para exportar.', 'danger'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `chips_virgens_${timestamp}`, + sheetName: 'ChipsVirgens', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, + { header: 'Numero do Chip', value: (row) => row.numeroDoChip ?? '' }, + { header: 'Observacoes', value: (row) => row.observacoes ?? '' }, + { 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'); + return; + } + + const baseRows = [...(this.controleRows ?? [])].sort((a, b) => { + const byAno = (this.toNullableNumber(a.ano) ?? 0) - (this.toNullableNumber(b.ano) ?? 0); + if (byAno !== 0) return byAno; + return (this.toNullableNumber(a.item) ?? 0) - (this.toNullableNumber(b.item) ?? 0); + }); + const rows = await this.fetchDetailedControleRowsForExport(baseRows); + + if (!rows.length) { + this.showToast('Nenhum registro encontrado para exportar.', 'danger'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `controle_recebidos_${timestamp}`, + sheetName: 'ControleRecebidos', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Ano', type: 'number', value: (row) => this.toNullableNumber(row.ano) ?? 0 }, + { header: 'Item', type: 'number', value: (row) => this.toNullableNumber(row.item) ?? 0 }, + { header: 'Nota Fiscal', value: (row) => row.notaFiscal ?? '' }, + { header: 'Chip', value: (row) => row.chip ?? '' }, + { header: 'Serial', value: (row) => row.serial ?? '' }, + { header: 'Conteudo da NF', value: (row) => row.conteudoDaNf ?? '' }, + { header: 'Numero da Linha', value: (row) => row.numeroDaLinha ?? '' }, + { header: 'Valor Unitario', type: 'currency', value: (row) => this.toNullableNumber(row.valorUnit) ?? 0 }, + { header: 'Valor da NF', type: 'currency', value: (row) => this.toNullableNumber(row.valorDaNf) ?? 0 }, + { header: 'Data da NF', type: 'date', value: (row) => row.dataDaNf ?? '' }, + { header: 'Data do Recebimento', type: 'date', value: (row) => row.dataDoRecebimento ?? '' }, + { header: 'Quantidade', type: 'number', value: (row) => this.toNullableNumber(row.quantidade) ?? 0 }, + { header: 'Resumo', type: 'boolean', value: (row) => !!row.isResumo }, + { 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 fetchDetailedChipRowsForExport(rows: ChipVirgemListDto[]): Promise { + if (!rows.length) return []; + + const detailed: ChipVirgemListDto[] = []; + const chunkSize = 12; + + 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.getChipVirgemById(row.id)); + } catch { + return row; + } + }) + ); + detailed.push(...resolved); + } + + return detailed; + } + + private async fetchDetailedControleRowsForExport(rows: ControleRecebidoListDto[]): Promise { + if (!rows.length) return []; + + const detailed: ControleRecebidoListDto[] = []; + const chunkSize = 12; + + 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.getControleRecebidoById(row.id)); + } catch { + return row; + } + }) + ); + detailed.push(...resolved); + } + + return detailed; + } + setControleSort(key: ControleSortKey) { if (this.controleSortBy === key) { this.controleSortDir = this.controleSortDir === 'asc' ? 'desc' : 'asc'; @@ -720,27 +856,19 @@ export class ChipsControleRecebidos implements OnInit, OnDestroy { get activePage() { return this.activeTab === 'chips' ? this.chipsPage : this.controlePage; } get activeTotal() { return this.activeTab === 'chips' ? this.chipsTotal : this.controleTotal; } get activePageSize() { return this.activeTab === 'chips' ? this.chipsPageSize : this.controlePageSize; } - get activeTotalPages() { return Math.max(1, Math.ceil((this.activeTotal || 0) / (this.activePageSize || 10))); } + get activeTotalPages() { return computeTotalPages(this.activeTotal || 0, this.activePageSize || 10); } - get activePageStart() { return this.activeTotal === 0 ? 0 : (this.activePage - 1) * this.activePageSize + 1; } - get activePageEnd() { return this.activeTotal === 0 ? 0 : Math.min(this.activePage * this.activePageSize, this.activeTotal); } + get activePageStart() { return computePageStart(this.activeTotal || 0, this.activePage, this.activePageSize); } + get activePageEnd() { return computePageEnd(this.activeTotal || 0, this.activePage, this.activePageSize); } get activeLoading() { return this.activeTab === 'chips' ? this.chipsLoading : this.controleLoading; } // ✅ novo get activePageNumbers() { - const total = this.activeTotalPages; - const current = this.activePage; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - const pages = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.activePage, this.activeTotalPages); } goToPage(p: number) { - const target = Math.max(1, Math.min(this.activeTotalPages, p)); + const target = clampPage(p, this.activeTotalPages); if (this.activeTab === 'chips') { this.chipsPage = target; diff --git a/src/app/pages/dados-usuarios/dados-usuarios.html b/src/app/pages/dados-usuarios/dados-usuarios.html index a3ebad1..4b468e1 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.html +++ b/src/app/pages/dados-usuarios/dados-usuarios.html @@ -32,6 +32,10 @@ + @@ -88,7 +92,6 @@ Itens por pág:
-
@@ -181,278 +184,4 @@ - - - + diff --git a/src/app/pages/dados-usuarios/dados-usuarios.scss b/src/app/pages/dados-usuarios/dados-usuarios.scss index 5ac4967..8455861 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.scss +++ b/src/app/pages/dados-usuarios/dados-usuarios.scss @@ -350,8 +350,6 @@ .pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; } /* MODALS */ -.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } -.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: clamp(12px, 2.2vw, 20px); } .modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; min-height: 0; } .modal-xl-custom { width: min(1050px, 95vw); max-height: 86vh; } @keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } diff --git a/src/app/pages/dados-usuarios/dados-usuarios.ts b/src/app/pages/dados-usuarios/dados-usuarios.ts index b94a1c9..325dade 100644 --- a/src/app/pages/dados-usuarios/dados-usuarios.ts +++ b/src/app/pages/dados-usuarios/dados-usuarios.ts @@ -2,7 +2,10 @@ 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 { DadosUsuariosModalsComponent } from '../../components/page-modals/dados-usuarios-modals/dados-usuarios-modals'; +import { TableExportService } from '../../services/table-export.service'; import { DadosUsuariosService, @@ -16,6 +19,13 @@ import { import { AuthService } from '../../services/auth.service'; import { LinesService, MobileLineDetail } from '../../services/lines.service'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; type ViewMode = 'lines' | 'groups'; @@ -36,15 +46,17 @@ interface SimpleOption { @Component({ selector: 'app-dados-usuarios', standalone: true, - imports: [CommonModule, FormsModule, CustomSelectComponent], + imports: [CommonModule, FormsModule, CustomSelectComponent, DadosUsuariosModalsComponent], templateUrl: './dados-usuarios.html', styleUrls: ['./dados-usuarios.scss'] }) export class DadosUsuarios implements OnInit { + readonly vm = this; @ViewChild('successToast', { static: false }) successToast!: ElementRef; loading = false; + exporting = false; errorMsg = ''; // Filtros @@ -116,7 +128,8 @@ export class DadosUsuarios implements OnInit { constructor( private service: DadosUsuariosService, private authService: AuthService, - private linesService: LinesService + private linesService: LinesService, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -136,26 +149,17 @@ export class DadosUsuarios implements OnInit { } get totalPages(): number { - return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10))); + return computeTotalPages(this.total || 0, this.pageSize || 10); } - get pageStart(): number { return (this.page - 1) * this.pageSize + 1; } + get pageStart(): number { return computePageStart(this.total || 0, this.page, this.pageSize); } get pageEnd(): number { - const end = this.page * this.pageSize; - return end > this.total ? this.total : end; + return computePageEnd(this.total || 0, this.page, this.pageSize); } get pageNumbers(): number[] { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } fetch(goToPage?: number): 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; @@ -263,7 +373,7 @@ export class DadosUsuarios implements OnInit { } goToPage(p: number) { - this.page = p; + this.page = clampPage(p, this.totalPages); this.fetch(); } diff --git a/src/app/pages/dashboard/dashboard.ts b/src/app/pages/dashboard/dashboard.ts index bb22e3b..31ce174 100644 --- a/src/app/pages/dashboard/dashboard.ts +++ b/src/app/pages/dashboard/dashboard.ts @@ -21,6 +21,7 @@ import { LineTotal, } from '../../services/resumo.service'; import { AuthService } from '../../services/auth.service'; +import { buildApiBaseUrl } from '../../utils/api-base.util'; // --- Interfaces (Mantidas intactas para não quebrar contrato) --- type KpiCard = { @@ -384,8 +385,7 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { private router: Router, @Inject(PLATFORM_ID) private platformId: object ) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } ngOnInit(): void { @@ -393,7 +393,8 @@ export class Dashboard implements OnInit, AfterViewInit, OnDestroy { const isSysAdmin = this.authService.hasRole('sysadmin'); const isGestor = this.authService.hasRole('gestor'); - this.isCliente = !(isSysAdmin || isGestor); + const isFinanceiro = this.authService.hasRole('financeiro'); + this.isCliente = !(isSysAdmin || isGestor || isFinanceiro); if (this.isCliente) { this.loadClientDashboardData(); diff --git a/src/app/pages/faturamento/faturamento.html b/src/app/pages/faturamento/faturamento.html index b8dc60b..9b16bca 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 @@
-
@@ -327,248 +331,5 @@ - - - - + diff --git a/src/app/pages/faturamento/faturamento.scss b/src/app/pages/faturamento/faturamento.scss index 2c9e0ec..bdf4e83 100644 --- a/src/app/pages/faturamento/faturamento.scss +++ b/src/app/pages/faturamento/faturamento.scss @@ -754,24 +754,6 @@ .fw-black { font-weight: 950; } /* MODALS (mantidos do seu arquivo) */ -.modal-backdrop-custom { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.45); - z-index: 9990; - backdrop-filter: blur(4px); -} - -.modal-custom { - position: fixed; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 9995; - padding: 16px; -} - .modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); diff --git a/src/app/pages/faturamento/faturamento.ts b/src/app/pages/faturamento/faturamento.ts index 0c46cbb..826d41c 100644 --- a/src/app/pages/faturamento/faturamento.ts +++ b/src/app/pages/faturamento/faturamento.ts @@ -13,6 +13,7 @@ import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { FaturamentoModalsComponent } from '../../components/page-modals/faturamento-modals/faturamento-modals'; import { BillingService, @@ -25,7 +26,17 @@ 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 { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; +import { normalizeAccentInsensitive } from '../../utils/text-normalization.util'; +import { firstValueFrom } from 'rxjs'; interface BillingClientGroup { cliente: string; @@ -38,11 +49,12 @@ interface BillingClientGroup { @Component({ standalone: true, - imports: [CommonModule, FormsModule, HttpClientModule, CustomSelectComponent], + imports: [CommonModule, FormsModule, HttpClientModule, CustomSelectComponent, FaturamentoModalsComponent], templateUrl: './faturamento.html', styleUrls: ['./faturamento.scss'] }) export class Faturamento implements AfterViewInit, OnDestroy { + readonly vm = this; toastMessage = ''; @ViewChild('successToast', { static: false }) successToast!: ElementRef; @@ -54,10 +66,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 = ''; @@ -218,15 +232,6 @@ export class Faturamento implements AfterViewInit, OnDestroy { return s ? s : '—'; } - private normalizeText(s: any): string { - return (s ?? '') - .toString() - .trim() - .toUpperCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); - } - private buildGlobalSearchBlob(row: BillingItem): string { const parts = [ row.tipo, @@ -242,13 +247,13 @@ export class Faturamento implements AfterViewInit, OnDestroy { row.formaPagamento, ]; - return this.normalizeText(parts.join(' ')); + return normalizeAccentInsensitive(parts.join(' ')); } private matchesTipo(itemTipo: any, filtro: TipoFiltro): boolean { if (filtro === 'ALL') return true; - const t = this.normalizeText(itemTipo); + const t = normalizeAccentInsensitive(itemTipo); if (filtro === 'PF') return t === 'PF' || t.includes('FISICA'); if (filtro === 'PJ') return t === 'PJ' || t.includes('JURIDICA'); @@ -415,6 +420,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(); @@ -549,16 +633,16 @@ export class Faturamento implements AfterViewInit, OnDestroy { let arr = [...baseTipo]; if (this.selectedClients.length > 0) { - const set = new Set(this.selectedClients.map((x) => this.normalizeText(x))); - arr = arr.filter((r) => set.has(this.normalizeText(r.cliente))); + const set = new Set(this.selectedClients.map((x) => normalizeAccentInsensitive(x))); + arr = arr.filter((r) => set.has(normalizeAccentInsensitive(r.cliente))); } - const term = this.normalizeText(this.searchTerm); - const resolvedClientsSet = new Set((this.searchResolvedClients ?? []).map((x) => this.normalizeText(x))); + const term = normalizeAccentInsensitive(this.searchTerm); + const resolvedClientsSet = new Set((this.searchResolvedClients ?? []).map((x) => normalizeAccentInsensitive(x))); if (term) { arr = arr.filter((r) => this.buildGlobalSearchBlob(r).includes(term) || - (resolvedClientsSet.size > 0 && resolvedClientsSet.has(this.normalizeText(r.cliente))) + (resolvedClientsSet.size > 0 && resolvedClientsSet.has(normalizeAccentInsensitive(r.cliente))) ); } @@ -576,7 +660,7 @@ export class Faturamento implements AfterViewInit, OnDestroy { totalLinhas += Number(r.qtdLinhas ?? 0) || 0; - const key = this.normalizeText(c); + const key = normalizeAccentInsensitive(c); if (!key) continue; const vivo = Number(r.valorContratoVivo ?? 0) || 0; @@ -652,7 +736,7 @@ export class Faturamento implements AfterViewInit, OnDestroy { } goToPage(p: number) { - this.page = Math.max(1, Math.min(this.totalPages, p)); + this.page = clampPage(p, this.totalPages); this.applyGroupPagination(); this.cdr.detectChanges(); } @@ -666,27 +750,19 @@ export class Faturamento implements AfterViewInit, OnDestroy { } get totalPages() { - return Math.ceil((this.total || 0) / this.pageSize) || 1; + return computeTotalPages(this.total || 0, this.pageSize); } get pageStart() { - return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + return computePageStart(this.total || 0, this.page, this.pageSize); } get pageEnd() { - return this.total === 0 ? 0 : Math.min(this.page * this.pageSize, this.total); + return computePageEnd(this.total || 0, this.page, this.pageSize); } get pageNumbers() { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } // -------------------------- @@ -795,4 +871,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 01bb3cf..2937efe 100644 --- a/src/app/pages/geral/geral.html +++ b/src/app/pages/geral/geral.html @@ -31,23 +31,43 @@ Tabela de linhas e dados de telefonia -
- +
+
+ + + +
+ + +
+ +
+ + Selecionadas: {{ batchStatusSelectionCount }} + + + +
@@ -333,6 +373,20 @@ {{ reservaSelectedCount > 0 && reservaSelectedCount === groupLines.length ? 'Limpar seleção' : 'Selecionar todas' }} + + - @@ -419,7 +473,7 @@
- + @@ -538,7 +592,7 @@
- + @@ -578,1667 +632,4 @@
- - - - - - - + diff --git a/src/app/pages/geral/geral.scss b/src/app/pages/geral/geral.scss index e8dda84..38116a6 100644 --- a/src/app/pages/geral/geral.scss +++ b/src/app/pages/geral/geral.scss @@ -273,6 +273,29 @@ .controls { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; } .search-group { max-width: 270px; border-radius: 12px; overflow: hidden; display: flex; align-items: stretch; background: #fff; border: 1px solid rgba(17, 18, 20, 0.15); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); transition: all 0.2s ease; &:focus-within { border-color: var(--brand); box-shadow: 0 4px 12px rgba(227, 61, 207, 0.15); transform: translateY(-1px); } .input-group-text { background: transparent; border: none; color: var(--muted); padding-left: 14px; padding-right: 8px; display: flex; align-items: center; i { font-size: 1rem; } } .form-control { border: none; background: transparent; padding: 10px 0; font-size: 0.9rem; color: var(--text); box-shadow: none; &::placeholder { color: rgba(17, 18, 20, 0.4); font-weight: 500; } &:focus { outline: none; } } .btn-clear { background: transparent; border: none; color: var(--muted); padding: 0 12px; display: flex; align-items: center; cursor: pointer; transition: color 0.2s; &:hover { color: #dc3545; } i { font-size: 1rem; } } } .page-size { margin-left: auto; @media (max-width: 500px) { margin-left: 0; width: 100%; justify-content: space-between; } } +.batch-status-tools { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-left: auto; + + @media (max-width: 900px) { + margin-left: 0; + width: 100%; + } +} +.batch-status-count { + font-size: 0.75rem; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgba(17, 18, 20, 0.62); + padding: 4px 8px; + border-radius: 999px; + border: 1px solid rgba(17, 18, 20, 0.12); + background: rgba(255, 255, 255, 0.8); +} .select-wrapper { position: relative; display: inline-block; min-width: 90px; } .select-glass { background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(17, 18, 20, 0.15); border-radius: 12px; color: var(--blue); font-weight: 800; font-size: 0.9rem; text-align: left; padding: 8px 36px 8px 14px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); cursor: pointer; transition: all 0.2s ease; width: 100%; &:hover { background: #fff; border-color: var(--blue); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(3, 15, 170, 0.1); } &:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.15); } } .select-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--muted); font-size: 0.75rem; transition: transform 0.2s ease; } @@ -500,8 +523,6 @@ /* ========================================================== */ /* 8. MODALS E FORMULÁRIOS COMPLETOS (RESTAURADOS ✅) */ /* ========================================================== */ -.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } -.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } .modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; } @keyframes modalPop { from { opacity: 0; transform: scale(0.95) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } @@ -581,6 +602,140 @@ padding: 20px 22px; } } +.modal-card.modal-batch-status { + width: min(1120px, 96vw); + max-height: 92vh; + + .modal-header { + padding: 18px 22px 16px; + align-items: flex-start; + gap: 12px; + flex-wrap: wrap; + } + + .modal-title { + flex: 1 1 320px; + min-width: 0; + line-height: 1.2; + } + + .batch-status-header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; + margin-left: auto; + } + + .details-dashboard { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + align-items: start; + + @media (max-width: 980px) { + grid-template-columns: 1fr; + gap: 12px; + } + } + + .modal-body { + padding: 18px 22px 22px; + } + + .detail-box { + height: 100%; + } + + .box-body { + padding: 14px 16px 16px; + } + + .form-grid { + gap: 14px 16px; + } + + .form-field { + gap: 8px; + } + + .reserva-confirmation-pills { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + margin-top: 2px; + + .summary-pill { + margin: 0; + white-space: normal; + line-height: 1.25; + } + } + + .batch-status-note { + display: block; + margin-top: 10px; + font-size: 0.8rem; + line-height: 1.4; + color: rgba(17, 18, 20, 0.58); + } + + @media (max-width: 980px) { + .modal-header { + padding: 16px 18px 14px; + } + + .batch-status-header-actions { + width: 100%; + justify-content: flex-end; + } + + .modal-body { + padding: 16px 18px 18px; + } + + .form-grid { + gap: 12px; + } + } + + @media (max-width: 640px) { + .modal-header { + padding: 14px 14px 12px; + } + + .modal-title { + font-size: 1rem; + gap: 10px; + } + + .batch-status-header-actions { + justify-content: stretch; + + .btn { + flex: 1 1 140px; + } + } + + .modal-body { + padding: 14px; + } + + .box-body { + padding: 12px; + } + + .reserva-confirmation-pills { + gap: 8px; + + .summary-pill { + width: 100%; + justify-content: flex-start; + } + } + } +} /* === MODAL DE EDITAR E SEÇÕES (Accordion) === */ /* ✅ Restauração Crítica para "Editar" e "Novo Cliente" */ diff --git a/src/app/pages/geral/geral.ts b/src/app/pages/geral/geral.ts index 3f3436c..0bd8044 100644 --- a/src/app/pages/geral/geral.ts +++ b/src/app/pages/geral/geral.ts @@ -19,13 +19,22 @@ import { } from '@angular/common/http'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { GeralModalsComponent } from '../../components/page-modals/geral-modals/geral-modals'; import { PlanAutoFillService } from '../../services/plan-autofill.service'; import { AuthService } from '../../services/auth.service'; import { TenantSyncService } from '../../services/tenant-sync.service'; +import { TableExportService } from '../../services/table-export.service'; import { SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; import { firstValueFrom, Subscription, filter } from 'rxjs'; import { environment } from '../../../environments/environment'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; +import { + buildPageNumbers, + clampPage, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; +import { buildApiEndpoint } from '../../utils/api-base.util'; import { BATCH_MASS_COLUMN_GUIDE, type BatchMassApplyMode, @@ -264,14 +273,48 @@ interface AssignReservaLinesResultDto { items: AssignReservaLineItemResultDto[]; } +type BatchStatusAction = 'BLOCK' | 'UNBLOCK'; + +interface BatchLineStatusUpdateRequestDto { + action: 'block' | 'unblock'; + blockStatus?: string | null; + applyToAllFiltered: boolean; + lineIds: string[]; + search?: string | null; + skil?: string | null; + clients?: string[]; + additionalMode?: string | null; + additionalServices?: string | null; + usuario?: string | null; +} + +interface BatchLineStatusUpdateItemResultDto { + id: string; + item?: number; + linha?: string | null; + usuario?: string | null; + statusAnterior?: string | null; + statusNovo?: string | null; + success: boolean; + message: string; +} + +interface BatchLineStatusUpdateResultDto { + requested: number; + updated: number; + failed: number; + items: BatchLineStatusUpdateItemResultDto[]; +} + @Component({ standalone: true, - imports: [CommonModule, FormsModule, CustomSelectComponent], + imports: [CommonModule, FormsModule, CustomSelectComponent, GeralModalsComponent], templateUrl: './geral.html', styleUrls: ['./geral.scss'] }) export class Geral implements OnInit, AfterViewInit, OnDestroy { + readonly vm = this; readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE; toastMessage = ''; @@ -293,22 +336,17 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { private router: Router, private route: ActivatedRoute, private tenantSyncService: TenantSyncService, - private solicitacoesLinhasService: SolicitacoesLinhasService + private solicitacoesLinhasService: SolicitacoesLinhasService, + private tableExportService: TableExportService ) {} - private readonly apiBase = (() => { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; - return `${apiBase}/lines`; - })(); - private readonly templatesApiBase = (() => { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; - return `${apiBase}/templates`; - })(); + private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'lines'); + private readonly templatesApiBase = buildApiEndpoint(environment.apiUrl, 'templates'); loading = false; + exporting = false; isSysAdmin = false; isGestor = false; + isFinanceiro = false; isClientRestricted = false; rows: LineRow[] = []; @@ -385,6 +423,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; @@ -398,6 +442,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { private keepPageOnNextGroupsLoad = false; private searchResolvedClient: string | null = null; + private searchRequestVersion = 0; private kpiRequestVersion = 0; private groupsRequestVersion = 0; private linesRequestVersion = 0; @@ -609,13 +654,57 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } get hasGroupLineSelectionTools(): boolean { - return !this.isClientRestricted && !!(this.expandedGroup ?? '').trim(); + return this.canManageLines && !!(this.expandedGroup ?? '').trim(); + } + + get canManageLines(): boolean { + return this.isSysAdmin || this.isGestor; } get canMoveSelectedLinesToReserva(): boolean { 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.canManageLines) 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; } @@ -739,7 +828,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (!isPlatformBrowser(this.platformId)) return; this.isSysAdmin = this.authService.hasRole('sysadmin'); this.isGestor = this.authService.hasRole('gestor'); - this.isClientRestricted = !(this.isSysAdmin || this.isGestor); + this.isFinanceiro = this.authService.hasRole('financeiro'); + this.isClientRestricted = !(this.isSysAdmin || this.isGestor || this.isFinanceiro); if (this.isClientRestricted) { this.filterSkil = 'ALL'; @@ -836,6 +926,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.groupLines = []; this.selectedClients = []; this.clientSearchTerm = ''; + this.searchTerm = ''; + this.searchResolvedClient = null; this.page = 1; } @@ -972,7 +1064,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() { @@ -1000,6 +1100,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; @@ -1019,8 +1120,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(); @@ -1094,6 +1198,8 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (this.searchTimer) clearTimeout(this.searchTimer); this.searchTimer = setTimeout(async () => { + const requestVersion = ++this.searchRequestVersion; + this.expandedGroup = null; this.groupLines = []; this.page = 1; @@ -1101,6 +1207,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { const term = (this.searchTerm ?? '').trim(); if (!term) { + if (requestVersion !== this.searchRequestVersion) return; this.searchResolvedClient = null; this.loadKpis(); this.loadGroups(); @@ -1110,16 +1217,23 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { if (this.isSpecificSearchTerm(term)) { const client = await this.resolveSearchToClient(term); + if (requestVersion !== this.searchRequestVersion) return; + if (client) { this.searchResolvedClient = client; this.loadKpis(); await this.loadOnlyThisClientGroup(client); + + if (requestVersion !== this.searchRequestVersion) return; + this.expandedGroup = client; this.fetchGroupLines(client, term); return; } } + if (requestVersion !== this.searchRequestVersion) return; + this.searchResolvedClient = null; this.loadKpis(); this.loadGroups(); @@ -1833,49 +1947,91 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.fetchGroupLines(clientName, useTerm); } + private async fetchAllGroupLines( + clientName: string, + search: string | undefined, + requestVersion: number + ): Promise { + try { + let baseParams = new HttpParams() + .set('client', clientName) + .set('sortBy', 'item') + .set('sortDir', 'asc'); + + baseParams = this.applyBaseFilters(baseParams); + + if (search) { + baseParams = baseParams.set('search', search); + } + + const pageSize = 5000; + let page = 1; + let expectedTotal = 0; + const allItems: ApiLineList[] = []; + + while (page <= 500) { + const params = baseParams + .set('page', String(page)) + .set('pageSize', String(pageSize)); + + const response = await firstValueFrom( + this.http.get>(this.apiBase, { + params: this.withNoCache(params) + }) + ); + + if (requestVersion !== this.linesRequestVersion) return; + + const items = response?.items ?? []; + expectedTotal = this.toInt(response?.total); + allItems.push(...items); + + if (items.length === 0) break; + if (items.length < pageSize) break; + if (expectedTotal > 0 && allItems.length >= expectedTotal) break; + + page += 1; + } + + if (requestVersion !== this.linesRequestVersion) return; + + const filteredItems = this.applyAdditionalFiltersClientSide(allItems); + this.groupLines = filteredItems.map((x) => this.mapApiLineListToLineRow(x)); + + this.loadingLines = false; + this.cdr.detectChanges(); + } catch { + if (requestVersion !== this.linesRequestVersion) return; + this.loadingLines = false; + await this.showToast('Erro ao carregar linhas do grupo.'); + } + } + + private mapApiLineListToLineRow(x: ApiLineList): LineRow { + return { + id: x.id, + item: String(x.item ?? ''), + linha: x.linha ?? '', + chip: x.chip ?? '', + cliente: x.cliente ?? '', + usuario: x.usuario ?? '', + centroDeCustos: x.centroDeCustos ?? '', + setorNome: x.setorNome ?? '', + aparelhoNome: x.aparelhoNome ?? '', + aparelhoCor: x.aparelhoCor ?? '', + status: x.status ?? '', + skil: x.skil ?? '', + contrato: x.vencConta ?? '' + }; + } + fetchGroupLines(clientName: string, search?: string) { const requestVersion = ++this.linesRequestVersion; this.groupLines = []; this.clearReservaSelection(); this.loadingLines = true; - let params = new HttpParams() - .set('client', clientName) - .set('page', '1') - .set('pageSize', '500') - .set('sortBy', 'item') - .set('sortDir', 'asc'); - params = this.applyBaseFilters(params); - - if (search) params = params.set('search', search); - - this.http.get>(this.apiBase, { params: this.withNoCache(params) }).subscribe({ - next: (res) => { - if (requestVersion !== this.linesRequestVersion) return; - const filteredItems = this.applyAdditionalFiltersClientSide(res.items ?? []); - this.groupLines = filteredItems.map((x) => ({ - id: x.id, - item: String(x.item ?? ''), - linha: x.linha ?? '', - chip: x.chip ?? '', - cliente: x.cliente ?? '', - usuario: x.usuario ?? '', - centroDeCustos: x.centroDeCustos ?? '', - setorNome: x.setorNome ?? '', - aparelhoNome: x.aparelhoNome ?? '', - aparelhoCor: x.aparelhoCor ?? '', - status: x.status ?? '', - skil: x.skil ?? '', - contrato: x.vencConta ?? '' - })); - this.loadingLines = false; - }, - error: () => { - if (requestVersion !== this.linesRequestVersion) return; - this.loadingLines = false; - this.showToast('Erro ao carregar linhas do grupo.'); - } - }); + void this.fetchAllGroupLines(clientName, search, requestVersion); } toggleClientMenu() { @@ -1969,7 +2125,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } goToPage(p: number) { - this.page = Math.max(1, Math.min(this.totalPages, p)); + this.page = clampPage(p, this.totalPages); this.refreshData({ keepCurrentPage: true }); } @@ -1984,7 +2140,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { get totalPages() { if (this.selectedClients.length > 0) return 1; if (this.searchResolvedClient) return 1; - return Math.ceil((this.total || 0) / this.pageSize) || 1; + return computeTotalPages(this.total || 0, this.pageSize); } get filteredCount() { @@ -1992,7 +2148,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } get pageStart() { - return this.filteredCount === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + return computePageStart(this.filteredCount, this.page, this.pageSize); } get pageEnd() { @@ -2010,15 +2166,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } get pageNumbers() { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } clearSearch() { @@ -2030,6 +2178,242 @@ 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 { + const parts: string[] = []; + + if (this.filterSkil === 'PF') parts.push('pf'); + else if (this.filterSkil === 'PJ') parts.push('pj'); + else if (this.filterSkil === 'RESERVA') parts.push('reserva'); + else parts.push('todas'); + + if (this.filterStatus === 'BLOCKED') { + if (this.blockedStatusMode === 'PERDA_ROUBO') parts.push('bloq-perda-roubo'); + else if (this.blockedStatusMode === 'BLOQUEIO_120') parts.push('bloq-120'); + else parts.push('bloqueadas'); + } + + if (this.additionalMode === 'WITH') parts.push('com-adicionais'); + else if (this.additionalMode === 'WITHOUT') parts.push('sem-adicionais'); + + if (this.selectedAdditionalServices.length > 0) { + parts.push(this.selectedAdditionalServices.join('-')); + } + + return parts.join('_'); + } + async onImportExcel() { if (!this.isSysAdmin) { await this.showToast('Você não tem permissão para importar planilha.'); @@ -2093,6 +2477,11 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onEditar(r: LineRow) { + if (this.isFinanceiro) { + await this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.editOpen = true; this.editSaving = false; this.requestSaving = false; @@ -2173,6 +2562,10 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async saveEdit() { + if (this.isFinanceiro) { + await this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } if (!this.editingId || !this.editModel || this.requestSaving) return; this.editSaving = true; @@ -2307,25 +2700,30 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async requestLineBlock() { - if (!this.editingId || this.requestSaving) return; - - this.requestSaving = true; - try { - await firstValueFrom(this.solicitacoesLinhasService.create({ - lineId: this.editingId, - tipoSolicitacao: 'bloqueio' - })); - - this.requestSaving = false; - this.closeAllModals(); - await this.showToast('Solicitação de bloqueio enviada.'); - } catch (err) { - this.requestSaving = false; - const msg = (err as HttpErrorResponse)?.error?.message || 'Erro ao enviar solicitação de bloqueio.'; - await this.showToast(msg); - } + if (!this.isClientRestricted) { + await this.showToast('Somente cliente pode solicitar bloqueio por essa ação.'); + return; } + if (!this.editingId || this.requestSaving) return; + + this.requestSaving = true; + try { + await firstValueFrom(this.solicitacoesLinhasService.create({ + lineId: this.editingId, + tipoSolicitacao: 'bloqueio' + })); + + this.requestSaving = false; + this.closeAllModals(); + await this.showToast('Solicitação de bloqueio enviada.'); + } catch (err) { + this.requestSaving = false; + const msg = (err as HttpErrorResponse)?.error?.message || 'Erro ao enviar solicitação de bloqueio.'; + await this.showToast(msg); + } +} + onAparelhoNotaFiscalSelected(event: Event) { const input = event.target as HTMLInputElement | null; this.aparelhoNotaFiscalFile = input?.files && input.files.length > 0 ? input.files[0] : null; @@ -2469,7 +2867,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onCadastrarLinha() { - if (this.isClientRestricted) { + if (!this.canManageLines) { await this.showToast('Você não tem permissão para cadastrar novos clientes.'); return; } @@ -2482,7 +2880,7 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { } async onAddLineToGroup(clientName: string) { - if (this.isClientRestricted) { + if (!this.canManageLines) { await this.showToast('Você não tem permissão para adicionar linhas.'); return; } @@ -3464,6 +3862,116 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { this.reservaSelectedLineIds = []; } + async openBatchStatusModal(action: BatchStatusAction) { + if (!this.canManageLines) { + 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.'); @@ -3885,6 +4393,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, @@ -3963,6 +4477,16 @@ export class Geral implements OnInit, AfterViewInit, OnDestroy { return Math.abs(a - b) < 0.000001; } + 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-linhas/historico-linhas.html b/src/app/pages/historico-linhas/historico-linhas.html new file mode 100644 index 0000000..eb67ad6 --- /dev/null +++ b/src/app/pages/historico-linhas/historico-linhas.html @@ -0,0 +1,278 @@ +
+ +
+ +
+ + + + + +
+
+
+
+
+ Linha +
+ +
+
Histórico de Linhas
+ Timeline completa das alterações feitas em uma linha específica. +
+ +
+ + +
+
+ +
+
+
+ + Filtros +
+
+ + +
+
+ +
+
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+
+ Eventos (filtro) + {{ total }} +
+
+ Status (página) + {{ statusCountInPage }} +
+
+ Trocas de Número (página) + {{ trocaCountInPage }} +
+
+ Mureg (página) + {{ muregCountInPage }} +
+
+
+ +
+
+
+ Informe a linha no filtro para carregar o histórico detalhado. +
+ +
+ +
+ + + +
+ Nenhuma alteração encontrada para a linha informada. +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Data/HoraUsuárioOrigemAçãoResumo da alteraçãoDetalhes
{{ formatDateTime(log.occurredAtUtc) }} +
+ {{ displayUserName(log) }} + {{ log.userEmail || '-' }} +
+
+ {{ log.page || '-' }} + + {{ formatAction(log.action) }} + + +
{{ summary.title }}
+
{{ summary.description }}
+
+ {{ formatChangeValue(summary.before) }} + + {{ formatChangeValue(summary.after) }} +
+
+ DDD: {{ formatChangeValue(summary.beforeDdd) }} {{ formatChangeValue(summary.afterDdd) }} +
+
+
+ +
+
+
+
+ Mudanças de campos +
+ +
+
+
+ {{ change.field }} + + {{ changeTypeLabel(change.changeType) }} + +
+
+ {{ formatChangeValue(change.oldValue) }} + + {{ formatChangeValue(change.newValue) }} +
+
+
+
+ +
Sem mudanças detalhadas nesse evento.
+
+
+
+
+
+
+ + +
+
+
diff --git a/src/app/pages/historico-linhas/historico-linhas.scss b/src/app/pages/historico-linhas/historico-linhas.scss new file mode 100644 index 0000000..1fe3352 --- /dev/null +++ b/src/app/pages/historico-linhas/historico-linhas.scss @@ -0,0 +1,648 @@ +:host { + --brand: #e33dcf; + --brand-soft: rgba(227, 61, 207, 0.12); + --blue: #030faa; + --text: #111214; + --muted: rgba(17, 18, 20, 0.64); + --surface: rgba(255, 255, 255, 0.9); + --surface-strong: #ffffff; + --line: rgba(15, 23, 42, 0.11); + --radius-xl: 22px; + --radius-lg: 16px; + --shadow-card: 0 20px 44px rgba(17, 18, 20, 0.1); + + display: block; + font-family: 'Inter', sans-serif; + color: var(--text); + box-sizing: border-box; +} + +.historico-linhas-page { + min-height: 100vh; + padding: 0 12px; + display: flex; + align-items: flex-start; + justify-content: center; + position: relative; + overflow-y: auto; + background: + radial-gradient(900px 420px at 20% 10%, rgba(227, 61, 207, 0.14), transparent 60%), + radial-gradient(820px 380px at 80% 30%, rgba(227, 61, 207, 0.08), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f5f5f7 70%); + + &::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: rgba(255, 255, 255, 0.25); + } +} + +.page-blob { + position: fixed; + pointer-events: none; + border-radius: 999px; + filter: blur(34px); + opacity: 0.55; + z-index: 0; + background: radial-gradient(circle at 30% 30%, rgba(227, 61, 207, 0.55), rgba(227, 61, 207, 0.06)); + animation: floaty 10s ease-in-out infinite; + + &.blob-1 { width: 420px; height: 420px; top: -140px; left: -140px; } + &.blob-2 { width: 520px; height: 520px; top: -220px; right: -240px; animation-duration: 12s; } + &.blob-3 { width: 360px; height: 360px; bottom: -180px; left: 25%; animation-duration: 14s; } + &.blob-4 { width: 520px; height: 520px; bottom: -260px; right: -260px; animation-duration: 16s; opacity: 0.45; } +} + +@keyframes floaty { + 0% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(18px, 10px) scale(1.03); } + 100% { transform: translate(0, 0) scale(1); } +} + +.container-geral-responsive { + width: 98% !important; + max-width: 1500px !important; + position: relative; + z-index: 1; + margin-top: 40px; + margin-bottom: 200px; +} + +.geral-card { + border-radius: var(--radius-xl); + overflow: hidden; + background: var(--surface); + border: 1px solid rgba(227, 61, 207, 0.16); + backdrop-filter: blur(12px); + box-shadow: var(--shadow-card); + position: relative; + display: flex; + flex-direction: column; + min-height: 80vh; + + &::before { + content: ''; + position: absolute; + inset: 1px; + border-radius: calc(var(--radius-xl) - 1px); + pointer-events: none; + border: 1px solid rgba(255, 255, 255, 0.65); + opacity: 0.75; + } +} + +.geral-header { + padding: 16px 24px; + border-bottom: 1px solid rgba(17, 18, 20, 0.06); + background: linear-gradient(180deg, rgba(227, 61, 207, 0.06), rgba(255, 255, 255, 0.2)); + flex-shrink: 0; +} + +.header-row-top { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 12px; + + @media (max-width: 900px) { + grid-template-columns: 1fr; + text-align: center; + gap: 14px; + + .title-badge { justify-self: center; } + .header-actions { justify-self: center; } + } +} + +.title-badge { + justify-self: start; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.82); + border: 1px solid rgba(227, 61, 207, 0.22); + color: var(--text); + font-size: 13px; + font-weight: 800; + + i { color: var(--brand); } +} + +.header-title { + justify-self: center; + text-align: center; +} + +.title { + font-size: 26px; + font-weight: 950; + letter-spacing: -0.3px; + color: var(--text); + margin-top: 10px; +} + +.subtitle { + color: var(--muted); + font-weight: 700; +} + +.header-actions { + justify-self: end; +} + +.btn-brand { + background-color: var(--brand); + border-color: var(--brand); + color: #fff; + font-weight: 900; + border-radius: 12px; + transition: transform 0.2s, box-shadow 0.2s; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(227, 61, 207, 0.25); + filter: brightness(1.05); + } +} + +.btn-glass { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(17, 18, 20, 0.16); + color: rgba(17, 18, 20, 0.85); + border-radius: 12px; + font-weight: 700; +} + +.filters-card { + background: rgba(255, 255, 255, 0.92); + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 16px; + padding: 16px; + display: grid; + gap: 14px; + box-shadow: 0 14px 28px rgba(17, 18, 20, 0.08); +} + +.filters-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.filters-title { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 900; + font-size: 14px; + color: rgba(17, 18, 20, 0.82); +} + +.filters-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.filters-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 12px; +} + +.filter-field { + display: grid; + gap: 6px; + grid-column: span 2; + min-width: 0; + + label { + font-size: 11px; + font-weight: 800; + color: rgba(17, 18, 20, 0.6); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + input { + width: 100%; + height: 40px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.15); + padding: 0 12px; + font-size: 14px; + background: #fff; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + } + + input:focus { + outline: none; + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(227, 61, 207, 0.12); + } +} + +.line-field { + grid-column: span 4; +} + +.period-field { + grid-column: span 3; +} + +.btn-primary, +.btn-ghost { + height: 38px; + border-radius: 10px; + border: none; + font-weight: 700; + font-size: 12px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0 14px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--brand), #bc30ac); + color: #fff; + box-shadow: 0 8px 16px rgba(227, 61, 207, 0.24); +} + +.btn-ghost { + background: rgba(15, 23, 42, 0.06); + color: rgba(15, 23, 42, 0.85); +} + +.kpi-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.kpi-card { + background: var(--surface-strong); + border: 1px solid rgba(17, 18, 20, 0.08); + border-radius: 14px; + padding: 12px 14px; + display: grid; + gap: 6px; + box-shadow: 0 8px 16px rgba(17, 18, 20, 0.06); +} + +.kpi-label { + font-size: 12px; + font-weight: 700; + color: rgba(17, 18, 20, 0.62); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.kpi-value { + font-size: 22px; + line-height: 1; + font-weight: 900; + color: var(--blue); +} + +.geral-body { + padding: 18px 24px; + flex: 1; +} + +.table-wrap { + width: 100%; + background: rgba(255, 255, 255, 0.84); + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 14px; + overflow: hidden; +} + +.table-modern { + margin: 0; + min-width: 980px; + + thead th { + background: linear-gradient(180deg, rgba(3, 15, 170, 0.92), rgba(3, 15, 170, 0.82)); + color: #fff; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.05em; + border: none; + padding: 12px; + white-space: nowrap; + } + + tbody td { + border-top: 1px solid rgba(15, 23, 42, 0.08); + vertical-align: middle; + padding: 12px; + background: rgba(255, 255, 255, 0.92); + } + + tbody tr.table-row-item:hover td { + background: rgba(227, 61, 207, 0.05); + } + + tbody tr.table-row-item.expanded td { + background: rgba(227, 61, 207, 0.08); + } +} + +.user-cell { + display: grid; + line-height: 1.2; +} + +.user-name { + font-weight: 800; +} + +.user-email { + color: rgba(17, 18, 20, 0.55); +} + +.origin-pill { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 4px 10px; + background: rgba(3, 15, 170, 0.1); + border: 1px solid rgba(3, 15, 170, 0.2); + color: rgba(3, 15, 170, 0.88); + font-size: 12px; + font-weight: 700; +} + +.badge-action { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 5px 10px; + font-size: 12px; + font-weight: 800; + border: 1px solid transparent; + + &.action-create { + color: #157347; + background: rgba(25, 135, 84, 0.12); + border-color: rgba(25, 135, 84, 0.24); + } + + &.action-update { + color: #0a58ca; + background: rgba(13, 110, 253, 0.12); + border-color: rgba(13, 110, 253, 0.24); + } + + &.action-delete { + color: #b02a37; + background: rgba(220, 53, 69, 0.12); + border-color: rgba(220, 53, 69, 0.24); + } + + &.action-default { + color: #495057; + background: rgba(108, 117, 125, 0.12); + border-color: rgba(108, 117, 125, 0.24); + } +} + +.summary-col { + min-width: 360px; +} + +.summary-title { + font-size: 13px; + font-weight: 900; + margin-bottom: 2px; +} + +.summary-description { + font-size: 12px; + color: rgba(17, 18, 20, 0.66); +} + +.summary-diff { + margin-top: 6px; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + border-radius: 10px; + background: rgba(15, 23, 42, 0.05); + padding: 4px 8px; + + .old { + color: #b02a37; + font-weight: 700; + } + + .new { + color: #157347; + font-weight: 700; + } +} + +.summary-ddd { + margin-top: 5px; + font-size: 11px; + color: rgba(17, 18, 20, 0.62); + font-weight: 700; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.tone-mureg { color: #005f73; } +.tone-troca { color: #6f42c1; } +.tone-status { color: #0a58ca; } +.tone-linha { color: #0d6efd; } +.tone-chip { color: #198754; } +.tone-generic { color: #495057; } + +.actions-col { + width: 84px; + text-align: center; +} + +.expand-btn { + width: 34px; + height: 34px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.15); + background: #fff; + color: rgba(15, 23, 42, 0.85); +} + +.details-row td { + background: rgba(255, 255, 255, 0.94); +} + +.details-panel { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} + +.details-section { + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 12px; + background: #fff; + padding: 10px 12px; +} + +.section-title { + display: inline-flex; + align-items: center; + gap: 7px; + font-weight: 800; + color: rgba(17, 18, 20, 0.84); + margin-bottom: 8px; +} + +.changes-list { + display: grid; + gap: 8px; +} + +.change-item { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 10px; + padding: 8px; + background: rgba(248, 249, 250, 0.85); +} + +.change-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 4px; +} + +.change-field { + font-size: 12px; + font-weight: 800; + color: #111; +} + +.change-type { + border-radius: 999px; + padding: 2px 8px; + font-size: 11px; + font-weight: 800; + border: 1px solid transparent; + + &.change-added { + background: rgba(25, 135, 84, 0.12); + color: #157347; + border-color: rgba(25, 135, 84, 0.24); + } + + &.change-removed { + background: rgba(220, 53, 69, 0.12); + color: #b02a37; + border-color: rgba(220, 53, 69, 0.24); + } + + &.change-modified { + background: rgba(13, 110, 253, 0.12); + color: #0a58ca; + border-color: rgba(13, 110, 253, 0.24); + } +} + +.change-values { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + + .old { + color: #b02a37; + font-weight: 700; + } + + .new { + color: #157347; + font-weight: 700; + } +} + +.empty-state { + font-size: 13px; + color: rgba(17, 18, 20, 0.62); + font-weight: 600; +} + +.empty-group { + text-align: center; + padding: 28px; + color: rgba(17, 18, 20, 0.65); + font-weight: 700; +} + +.empty-group.helper { + background: rgba(3, 15, 170, 0.05); + border-bottom: 1px solid rgba(3, 15, 170, 0.12); +} + +.geral-footer { + display: none; +} + +.footer-meta { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} + +.pagination-modern .page-link { + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.15); + color: rgba(15, 23, 42, 0.85); + margin: 0 2px; +} + +.pagination-modern .page-item.active .page-link { + background: var(--brand); + border-color: var(--brand); + color: #fff; +} + +@media (max-width: 1200px) { + .filters-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } + .filter-field { grid-column: span 2; } + .line-field { grid-column: span 3; } + .period-field { grid-column: span 3; } + .kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .details-panel { grid-template-columns: 1fr; } +} + +@media (max-width: 768px) { + .geral-header, + .geral-body, + .geral-footer { + padding-left: 14px; + padding-right: 14px; + } + + .filters-grid { grid-template-columns: repeat(1, minmax(0, 1fr)); } + .filter-field, + .line-field { + grid-column: span 1; + } + + .kpi-grid { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } +} diff --git a/src/app/pages/historico-linhas/historico-linhas.ts b/src/app/pages/historico-linhas/historico-linhas.ts new file mode 100644 index 0000000..9e1b5cd --- /dev/null +++ b/src/app/pages/historico-linhas/historico-linhas.ts @@ -0,0 +1,596 @@ +import { Component, OnInit, ElementRef, ViewChild, ChangeDetectorRef, Inject, PLATFORM_ID } from '@angular/core'; +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, + AuditFieldChangeDto, + LineHistoricoQuery +} from '../../services/historico.service'; +import { TableExportService } from '../../services/table-export.service'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; + +interface SelectOption { + value: string; + label: string; +} + +type EventTone = 'mureg' | 'troca' | 'status' | 'linha' | 'chip' | 'generic'; + +interface EventSummary { + title: string; + description: string; + before?: string | null; + after?: string | null; + beforeDdd?: string | null; + afterDdd?: string | null; + tone: EventTone; +} + +@Component({ + selector: 'app-historico-linhas', + standalone: true, + imports: [CommonModule, FormsModule, CustomSelectComponent], + templateUrl: './historico-linhas.html', + styleUrls: ['./historico-linhas.scss'], +}) +export class HistoricoLinhas implements OnInit { + @ViewChild('successToast', { static: false }) successToast!: ElementRef; + + logs: AuditLogDto[] = []; + loading = false; + exporting = false; + error = false; + errorMsg = ''; + toastMessage = ''; + + expandedLogId: string | null = null; + + page = 1; + pageSize = 10; + pageSizeOptions = [10, 20, 50, 100]; + total = 0; + + filterLine = ''; + filterPageName = ''; + filterAction = ''; + filterUser = ''; + dateFrom = ''; + dateTo = ''; + + readonly pageOptions: SelectOption[] = [ + { value: '', label: 'Todas as origens' }, + { value: 'Geral', label: 'Geral' }, + { value: 'Mureg', label: 'Mureg' }, + { value: 'Troca de número', label: 'Troca de número' }, + { value: 'Vigência', label: 'Vigência' }, + { value: 'Parcelamentos', label: 'Parcelamentos' }, + ]; + + readonly actionOptions: SelectOption[] = [ + { value: '', label: 'Todas as ações' }, + { value: 'CREATE', label: 'Criação' }, + { value: 'UPDATE', label: 'Atualização' }, + { value: 'DELETE', label: 'Exclusão' }, + ]; + + private readonly summaryCache = new Map(); + private readonly idFieldExceptions = new Set(['iccid']); + + constructor( + private readonly historicoService: HistoricoService, + private readonly cdr: ChangeDetectorRef, + @Inject(PLATFORM_ID) private readonly platformId: object, + private readonly tableExportService: TableExportService + ) {} + + ngOnInit(): void { + // Tela inicia aguardando o usuário informar a linha. + } + + applyFilters(): void { + this.page = 1; + this.fetch(); + } + + refresh(): void { + this.fetch(); + } + + clearFilters(): void { + this.filterLine = ''; + this.filterPageName = ''; + this.filterAction = ''; + this.filterUser = ''; + this.dateFrom = ''; + this.dateTo = ''; + this.page = 1; + this.logs = []; + this.total = 0; + this.error = false; + this.errorMsg = ''; + this.summaryCache.clear(); + } + + onPageSizeChange(): void { + this.page = 1; + this.fetch(); + } + + goToPage(target: number): void { + this.page = clampPage(target, this.totalPages); + this.fetch(); + } + + toggleDetails(log: AuditLogDto, event?: Event): void { + if (event) event.stopPropagation(); + this.expandedLogId = this.expandedLogId === log.id ? null : log.id; + } + + async onExport(): Promise { + if (this.exporting) return; + + const lineTerm = this.normalizedLineTerm; + if (!lineTerm) { + await this.showToast('Informe a linha para exportar.'); + return; + } + + this.exporting = true; + try { + const allLogs = await this.fetchAllLogsForExport(); + if (!allLogs.length) { + await this.showToast('Nenhum evento encontrado para exportar.'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `historico_linhas_${timestamp}`, + sheetName: 'HistoricoLinhas', + rows: allLogs, + columns: [ + { header: 'Data/Hora', type: 'datetime', value: (log) => log.occurredAtUtc ?? '' }, + { header: 'Usuario', value: (log) => this.displayUserName(log) }, + { header: 'E-mail', value: (log) => log.userEmail ?? '' }, + { header: 'Origem', value: (log) => log.page ?? '' }, + { header: 'Acao', value: (log) => this.formatAction(log.action) }, + { header: 'Evento', value: (log) => this.summaryFor(log).title }, + { header: 'Resumo', value: (log) => this.summaryFor(log).description }, + { header: 'Valor Anterior', value: (log) => this.summaryFor(log).before ?? '' }, + { header: 'Valor Novo', value: (log) => this.summaryFor(log).after ?? '' }, + { header: 'DDD Anterior', value: (log) => this.summaryFor(log).beforeDdd ?? '' }, + { header: 'DDD Novo', value: (log) => this.summaryFor(log).afterDdd ?? '' }, + { header: 'Mudancas', value: (log) => this.formatChangesSummary(log) }, + ], + }); + + await this.showToast(`Planilha exportada com ${allLogs.length} evento(s).`); + } catch { + await this.showToast('Erro ao exportar histórico de linhas.'); + } finally { + this.exporting = false; + } + } + + formatDateTime(value?: string | null): string { + if (!value) return '-'; + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) return '-'; + return dt.toLocaleString('pt-BR'); + } + + displayUserName(log: AuditLogDto): string { + const name = (log.userName || '').trim(); + return name ? name : 'SISTEMA'; + } + + formatAction(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (!value) return '-'; + if (value === 'CREATE') return 'Criação'; + if (value === 'UPDATE') return 'Atualização'; + if (value === 'DELETE') return 'Exclusão'; + return 'Outro'; + } + + actionClass(action?: string | null): string { + const value = (action || '').toUpperCase(); + if (value === 'CREATE') return 'action-create'; + if (value === 'UPDATE') return 'action-update'; + if (value === 'DELETE') return 'action-delete'; + return 'action-default'; + } + + changeTypeLabel(type?: AuditChangeType | string | null): string { + if (!type) return 'Alterado'; + if (type === 'added') return 'Adicionado'; + if (type === 'removed') return 'Removido'; + return 'Alterado'; + } + + changeTypeClass(type?: AuditChangeType | string | null): string { + if (type === 'added') return 'change-added'; + if (type === 'removed') return 'change-removed'; + return 'change-modified'; + } + + formatChangeValue(value?: string | null): string { + if (value === undefined || value === null || value === '') return '-'; + return String(value); + } + + summaryFor(log: AuditLogDto): EventSummary { + const cached = this.summaryCache.get(log.id); + if (cached) return cached; + const summary = this.buildEventSummary(log); + this.summaryCache.set(log.id, summary); + return summary; + } + + toneClass(tone: EventTone): string { + return `tone-${tone}`; + } + + trackByLog(_: number, log: AuditLogDto): string { + return log.id; + } + + trackByField(_: number, change: AuditFieldChangeDto): string { + return `${change.field}-${change.oldValue ?? ''}-${change.newValue ?? ''}`; + } + + visibleChanges(log: AuditLogDto): AuditFieldChangeDto[] { + return this.publicChanges(log); + } + + get normalizedLineTerm(): string { + return (this.filterLine || '').trim(); + } + + get hasLineFilter(): boolean { + return !!this.normalizedLineTerm; + } + + get totalPages(): number { + return computeTotalPages(this.total || 0, this.pageSize); + } + + get pageNumbers(): number[] { + return buildPageNumbers(this.page, this.totalPages); + } + + get pageStart(): number { + return computePageStart(this.total || 0, this.page, this.pageSize); + } + + get pageEnd(): number { + return computePageEnd(this.total || 0, this.page, this.pageSize); + } + + get statusCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'status').length; + } + + get trocaCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'troca').length; + } + + get muregCountInPage(): number { + return this.logs.filter((log) => this.summaryFor(log).tone === 'mureg').length; + } + + private fetch(): void { + const lineTerm = this.normalizedLineTerm; + if (!lineTerm) { + this.logs = []; + this.total = 0; + this.error = true; + this.errorMsg = 'Informe a linha para consultar o histórico.'; + this.loading = false; + this.summaryCache.clear(); + return; + } + + this.loading = true; + this.error = false; + this.errorMsg = ''; + this.expandedLogId = null; + + const query: LineHistoricoQuery = { + ...this.buildBaseQuery(), + line: lineTerm, + page: this.page, + pageSize: this.pageSize, + }; + + this.historicoService.listByLine(query).subscribe({ + next: (res) => { + this.logs = res.items || []; + this.total = res.total || 0; + this.page = res.page || this.page; + this.pageSize = res.pageSize || this.pageSize; + this.loading = false; + this.rebuildSummaryCache(); + }, + error: (err: HttpErrorResponse) => { + this.loading = false; + this.error = true; + this.logs = []; + this.total = 0; + this.summaryCache.clear(); + if (err?.status === 400) { + this.errorMsg = err?.error?.message || 'Informe uma linha válida.'; + return; + } + if (err?.status === 403) { + this.errorMsg = 'Acesso restrito.'; + return; + } + this.errorMsg = 'Erro ao carregar histórico da linha. Tente novamente.'; + } + }); + } + + private async fetchAllLogsForExport(): Promise { + const lineTerm = this.normalizedLineTerm; + if (!lineTerm) return []; + + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const all: AuditLogDto[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.historicoService.listByLine({ + ...this.buildBaseQuery(), + line: lineTerm, + 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, + dateFrom: this.toIsoDate(this.dateFrom, false) || undefined, + dateTo: this.toIsoDate(this.dateTo, true) || undefined, + }; + } + + private rebuildSummaryCache(): void { + this.summaryCache.clear(); + this.logs.forEach((log) => { + this.summaryCache.set(log.id, this.buildEventSummary(log)); + }); + } + + private buildEventSummary(log: AuditLogDto): EventSummary { + const page = (log.page || '').toLowerCase(); + const entity = (log.entityName || '').toLowerCase(); + + const linhaChange = this.findChange(log, 'linha'); + const statusChange = this.findChange(log, 'status'); + const chipChange = this.findChange(log, 'chip', 'iccid'); + const linhaAntiga = this.findChange(log, 'linhaantiga'); + const linhaNova = this.findChange(log, 'linhanova'); + + const muregLike = entity === 'muregline' || page.includes('mureg'); + if (muregLike) { + const before = this.firstFilled(linhaAntiga?.newValue, linhaAntiga?.oldValue, linhaChange?.oldValue); + const after = this.firstFilled(linhaNova?.newValue, linhaNova?.oldValue, linhaChange?.newValue); + return { + title: 'Troca de Mureg', + description: 'Linha alterada no fluxo de Mureg.', + before, + after, + beforeDdd: this.extractDdd(before), + afterDdd: this.extractDdd(after), + tone: 'mureg', + }; + } + + const trocaLike = entity === 'trocanumeroline' || page.includes('troca'); + if (trocaLike) { + const before = this.firstFilled(linhaAntiga?.newValue, linhaAntiga?.oldValue, linhaChange?.oldValue); + const after = this.firstFilled(linhaNova?.newValue, linhaNova?.oldValue, linhaChange?.newValue); + return { + title: 'Troca de Número', + description: 'Linha antiga substituída por uma nova.', + before, + after, + beforeDdd: this.extractDdd(before), + afterDdd: this.extractDdd(after), + tone: 'troca', + }; + } + + if (statusChange) { + const oldStatus = this.firstFilled(statusChange.oldValue); + const newStatus = this.firstFilled(statusChange.newValue); + const wasBlocked = this.isBlockedStatus(oldStatus); + const isBlocked = this.isBlockedStatus(newStatus); + let description = 'Status da linha atualizado.'; + if (!wasBlocked && isBlocked) description = 'Linha foi bloqueada.'; + if (wasBlocked && !isBlocked) description = 'Linha foi desbloqueada.'; + return { + title: 'Status da Linha', + description, + before: oldStatus, + after: newStatus, + tone: 'status', + }; + } + + if (linhaChange) { + return { + title: 'Alteração da Linha', + description: 'Número da linha foi atualizado.', + before: this.firstFilled(linhaChange.oldValue), + after: this.firstFilled(linhaChange.newValue), + beforeDdd: this.extractDdd(linhaChange.oldValue), + afterDdd: this.extractDdd(linhaChange.newValue), + tone: 'linha', + }; + } + + if (chipChange) { + return { + title: 'Alteração de Chip', + description: 'ICCID/chip atualizado na linha.', + before: this.firstFilled(chipChange.oldValue), + after: this.firstFilled(chipChange.newValue), + tone: 'chip', + }; + } + + const first = this.publicChanges(log)[0]; + if (first) { + return { + title: 'Outras alterações', + description: `Campo ${first.field} foi atualizado.`, + before: this.firstFilled(first.oldValue), + after: this.firstFilled(first.newValue), + tone: 'generic', + }; + } + + return { + title: 'Sem detalhes', + description: 'Não há mudanças detalhadas registradas para este evento.', + tone: 'generic', + }; + } + + private findChange(log: AuditLogDto, ...fields: string[]): AuditFieldChangeDto | null { + if (!fields.length) return null; + const normalizedTargets = new Set(fields.map((field) => this.normalizeField(field))); + return (log.changes || []).find((change) => normalizedTargets.has(this.normalizeField(change.field))) || null; + } + + private normalizeField(value?: string | null): string { + return (value ?? '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-zA-Z0-9]/g, '') + .toLowerCase() + .trim(); + } + + private firstFilled(...values: Array): string | null { + for (const value of values) { + const normalized = (value ?? '').toString().trim(); + if (normalized) return normalized; + } + return null; + } + + private formatChangesSummary(log: AuditLogDto): string { + const changes = this.publicChanges(log); + 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 publicChanges(log: AuditLogDto): AuditFieldChangeDto[] { + return (log?.changes ?? []).filter((change) => !this.isHiddenIdField(change?.field)); + } + + private isHiddenIdField(field?: string | null): boolean { + const normalized = this.normalizeField(field); + if (!normalized) return false; + if (this.idFieldExceptions.has(normalized)) return false; + if (normalized === 'id') return true; + return normalized.endsWith('id'); + } + + private isBlockedStatus(status?: string | null): boolean { + const normalized = (status ?? '').toLowerCase().trim(); + if (!normalized) return false; + return ( + normalized.includes('bloque') || + normalized.includes('perda') || + normalized.includes('roubo') || + normalized.includes('suspens') + ); + } + + private extractDdd(value?: string | null): string | null { + const digits = this.digitsOnly(value); + if (!digits) return null; + + if (digits.startsWith('55') && digits.length >= 12) { + return digits.slice(2, 4); + } + if (digits.length >= 10) { + return digits.slice(0, 2); + } + if (digits.length >= 2) { + return digits.slice(0, 2); + } + return null; + } + + private digitsOnly(value?: string | null): string { + return (value ?? '').replace(/\D/g, ''); + } + + private toIsoDate(value: string, endOfDay: boolean): string | null { + if (!value) return null; + const time = endOfDay ? '23:59:59' : '00:00:00'; + const date = new Date(`${value}T${time}`); + if (isNaN(date.getTime())) return null; + return date.toISOString(); + } + + 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/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..6dbd52b 100644 --- a/src/app/pages/historico/historico.ts +++ b/src/app/pages/historico/historico.ts @@ -2,9 +2,18 @@ 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'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; interface SelectOption { value: string; @@ -23,6 +32,7 @@ export class Historico implements OnInit { logs: AuditLogDto[] = []; loading = false; + exporting = false; error = false; errorMsg = ''; toastMessage = ''; @@ -65,7 +75,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,35 +122,66 @@ 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.page = clampPage(p, this.totalPages); this.fetch(); } get totalPages(): number { - return Math.ceil((this.total || 0) / this.pageSize) || 1; + return computeTotalPages(this.total || 0, this.pageSize); } get pageNumbers(): number[] { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } get pageStart(): number { - return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + return computePageStart(this.total || 0, this.page, this.pageSize); } get pageEnd(): number { - if (this.total === 0) return 0; - return Math.min(this.page * this.pageSize, this.total); + return computePageEnd(this.total || 0, this.page, this.pageSize); } toggleDetails(log: AuditLogDto, event?: Event): void { @@ -217,14 +259,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 +284,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..ba4d93b 100644 --- a/src/app/pages/mureg/mureg.html +++ b/src/app/pages/mureg/mureg.html @@ -31,7 +31,11 @@
- +
@@ -173,10 +177,10 @@ - - @@ -218,316 +222,4 @@ - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/app/pages/mureg/mureg.scss b/src/app/pages/mureg/mureg.scss index 3e30c8f..186021a 100644 --- a/src/app/pages/mureg/mureg.scss +++ b/src/app/pages/mureg/mureg.scss @@ -277,8 +277,6 @@ .pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; } /* MODALS */ -.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } -.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } .modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); border-radius: 20px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; display: flex; flex-direction: column; animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); width: min(850px, 100%); max-height: 90vh; } .modal-card.modal-xl-custom { width: min(920px, 90vw); max-height: 78vh; } .modal-card.modal-sm { width: min(480px, 100%); } diff --git a/src/app/pages/mureg/mureg.ts b/src/app/pages/mureg/mureg.ts index a09f8f8..aee7365 100644 --- a/src/app/pages/mureg/mureg.ts +++ b/src/app/pages/mureg/mureg.ts @@ -10,10 +10,22 @@ import { import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { AuthService } from '../../services/auth.service'; 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'; +import { MuregModalsComponent } from '../../components/page-modals/mureg-modals/mureg-modals'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; +import { buildApiEndpoint } from '../../utils/api-base.util'; type MuregKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataDaMureg' | 'cliente'; @@ -75,15 +87,28 @@ 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], + imports: [CommonModule, FormsModule, CustomSelectComponent, MuregModalsComponent], templateUrl: './mureg.html', styleUrls: ['./mureg.scss'] }) export class Mureg implements AfterViewInit { + readonly vm = this; toastMessage = ''; loading = false; + exporting = false; @ViewChild('successToast', { static: false }) successToast!: ElementRef; @@ -91,14 +116,12 @@ export class Mureg implements AfterViewInit { @Inject(PLATFORM_ID) private platformId: object, private http: HttpClient, private cdr: ChangeDetectorRef, - private linesService: LinesService + private authService: AuthService, + private linesService: LinesService, + private tableExportService: TableExportService ) {} - private readonly apiBase = (() => { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; - return `${apiBase}/mureg`; - })(); + private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'mureg'); // ====== DATA ====== clientGroups: ClientGroup[] = []; @@ -162,9 +185,20 @@ export class Mureg implements AfterViewInit { clienteInfo: '' }; + isSysAdmin = false; + isGestor = false; + isFinanceiro = false; + + get canManageRecords(): boolean { + return this.isSysAdmin || this.isGestor; + } + async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; this.initAnimations(); + this.isSysAdmin = this.authService.hasRole('sysadmin'); + this.isGestor = this.authService.hasRole('gestor'); + this.isFinanceiro = this.authService.hasRole('financeiro'); setTimeout(() => { this.preloadClients(); // ✅ já deixa o select pronto this.refresh(); @@ -184,6 +218,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(() => { @@ -208,29 +383,20 @@ export class Mureg implements AfterViewInit { } goToPage(p: number) { - this.page = Math.max(1, Math.min(this.totalPages, p)); + this.page = clampPage(p, this.totalPages); this.applyPagination(); } - get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; } + get totalPages() { return computeTotalPages(this.total || 0, this.pageSize); } get pageNumbers() { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } - get pageStart() { return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; } + get pageStart() { return computePageStart(this.total || 0, this.page, this.pageSize); } get pageEnd() { - if (this.total === 0) return 0; - return Math.min(this.page * this.pageSize, this.total); + return computePageEnd(this.total || 0, this.page, this.pageSize); } trackById(_: number, row: MuregRow) { return row.id; } @@ -468,6 +634,11 @@ export class Mureg implements AfterViewInit { // CREATE MODAL // ======================================================================= onCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.preloadClients(); this.createOpen = true; @@ -480,7 +651,7 @@ export class Mureg implements AfterViewInit { linhaAntiga: '', linhaNova: '', iccid: '', - dataDaMureg: '', + dataDaMureg: this.nowDateInput(), clienteInfo: '' }; @@ -507,6 +678,11 @@ export class Mureg implements AfterViewInit { } saveCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + const mobileLineId = String(this.createModel.mobileLineId ?? '').trim(); const linhaNova = String(this.createModel.linhaNova ?? '').trim(); @@ -523,7 +699,7 @@ export class Mureg implements AfterViewInit { linhaAntiga: (this.createModel.linhaAntiga ?? '') || null, linhaNova: (this.createModel.linhaNova ?? '') || null, iccid: (this.createModel.iccid ?? '') || null, - dataDaMureg: this.dateInputToIso(this.createModel.dataDaMureg) + dataDaMureg: new Date().toISOString() }; if (!payload.item || payload.item <= 0) delete payload.item; @@ -547,6 +723,11 @@ export class Mureg implements AfterViewInit { // EDIT MODAL // ======================================================================= onEditar(r: MuregRow) { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.preloadClients(); this.editOpen = true; @@ -614,6 +795,11 @@ export class Mureg implements AfterViewInit { } saveEdit() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + if (!this.editModel || !this.editModel.id) return; const mobileLineId = String(this.editModel.mobileLineId ?? '').trim(); @@ -688,6 +874,11 @@ export class Mureg implements AfterViewInit { // DELETE MODAL // ======================================================================= onDelete(row: MuregRow) { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.deleteTarget = row; this.deleteOpen = true; this.deleteSaving = false; @@ -700,6 +891,11 @@ export class Mureg implements AfterViewInit { } async confirmDelete() { + if (!this.canManageRecords) { + await this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + if (!this.deleteTarget?.id) return; if (!(await confirmDeletionWithTyping('esta Mureg'))) return; @@ -758,6 +954,14 @@ export class Mureg implements AfterViewInit { return dt.toISOString(); } + private nowDateInput(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + private extractApiMessage(err: any): string | null { try { const m1 = err?.error?.message; @@ -770,6 +974,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/parcelamentos.html b/src/app/pages/parcelamentos/parcelamentos.html index 1697b20..948688d 100644 --- a/src/app/pages/parcelamentos/parcelamentos.html +++ b/src/app/pages/parcelamentos/parcelamentos.html @@ -1,4 +1,14 @@
+
+
+
+ LineGestao + +
+
{{ toastMessage }}
+
+
+
@@ -65,7 +79,8 @@ [total]="total" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" - [isSysAdmin]="isSysAdmin" + [canEdit]="$any(canManageRecords)" + [canDelete]="$any(isSysAdmin)" (segmentChange)="setSegment($event)" (detail)="openDetails($event)" (edit)="openEdit($event)" @@ -77,175 +92,4 @@
- -
-
-
- - - - - -
-
- - - - - - - - -
-
- -
+ diff --git a/src/app/pages/parcelamentos/parcelamentos.scss b/src/app/pages/parcelamentos/parcelamentos.scss index 6c25851..8a34e91 100644 --- a/src/app/pages/parcelamentos/parcelamentos.scss +++ b/src/app/pages/parcelamentos/parcelamentos.scss @@ -226,26 +226,6 @@ color: var(--pg-primary-strong); } -.lg-backdrop { - position: fixed; - inset: 0; - background: - radial-gradient(circle at 15% 0%, rgba(31, 79, 214, 0.16), rgba(15, 23, 42, 0.64) 42%), - rgba(15, 23, 42, 0.6); - z-index: 9990; - backdrop-filter: blur(4px); -} - -.lg-modal { - position: fixed; - inset: 0; - z-index: 9995; - display: flex; - align-items: center; - justify-content: center; - padding: 16px; -} - .lg-modal-card { width: min(1180px, 98vw); max-height: 92vh; diff --git a/src/app/pages/parcelamentos/parcelamentos.ts b/src/app/pages/parcelamentos/parcelamentos.ts index 0b2d3f1..30ec4bc 100644 --- a/src/app/pages/parcelamentos/parcelamentos.ts +++ b/src/app/pages/parcelamentos/parcelamentos.ts @@ -2,8 +2,10 @@ import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { environment } from '../../../environments/environment'; -import { finalize, Subscription, timeout } from 'rxjs'; +import { finalize, Subscription, firstValueFrom, timeout } from 'rxjs'; +import { ParcelamentosModalsComponent } from '../../components/page-modals/parcelamento-modals/parcelamentos-modals'; import { AuthService } from '../../services/auth.service'; +import { TableExportService } from '../../services/table-export.service'; import { ParcelamentosService, ParcelamentoListItem, @@ -18,27 +20,35 @@ import { import { ParcelamentosKpisComponent, ParcelamentoKpi, -} from './components/parcelamentos-kpis/parcelamentos-kpis'; +} from '../../components/page-modals/parcelamento-modals/components/parcelamentos-kpis/parcelamentos-kpis'; import { ParcelamentosFiltersComponent, ParcelamentosFiltersModel, FilterChip, -} from './components/parcelamentos-filters/parcelamentos-filters'; +} from '../../components/page-modals/parcelamento-modals/components/parcelamentos-filters/parcelamentos-filters'; import { ParcelamentosTableComponent, ParcelamentoSegment, ParcelamentoViewItem, -} from './components/parcelamentos-table/parcelamentos-table'; +} from '../../components/page-modals/parcelamento-modals/components/parcelamentos-table/parcelamentos-table'; import { - ParcelamentoCreateModalComponent, ParcelamentoCreateModel, -} from './components/parcelamento-create-modal/parcelamento-create-modal'; +} from '../../components/page-modals/parcelamento-modals/components/parcelamento-create-modal/parcelamento-create-modal'; import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; +import { normalizeAccentInsensitive } from '../../utils/text-normalization.util'; type MonthOption = { value: number; label: string }; type ParcelamentoStatus = 'ativos' | 'futuros' | 'finalizados'; type AnnualMonthValue = { month: number; label: string; value: number | null }; type AnnualRow = { year: number; total: number; months: AnnualMonthValue[] }; +type ParcelamentoExportRow = ParcelamentoViewItem & Partial; @Component({ selector: 'app-parcelamentos', @@ -46,17 +56,23 @@ type AnnualRow = { year: number; total: number; months: AnnualMonthValue[] }; imports: [ CommonModule, FormsModule, + ParcelamentosModalsComponent, ParcelamentosKpisComponent, ParcelamentosFiltersComponent, ParcelamentosTableComponent, - ParcelamentoCreateModalComponent, ], templateUrl: './parcelamentos.html', styleUrls: ['./parcelamentos.scss'], }) export class Parcelamentos implements OnInit, OnDestroy { + readonly vm = this; loading = false; + exporting = false; errorMessage = ''; + toastOpen = false; + toastMessage = ''; + toastType: 'success' | 'danger' = 'success'; + private toastTimer: ReturnType | null = null; debugMode = !environment.production; @@ -88,6 +104,12 @@ export class Parcelamentos implements OnInit, OnDestroy { activeChips: FilterChip[] = []; isSysAdmin = false; + isGestor = false; + isFinanceiro = false; + + get canManageRecords(): boolean { + return this.isSysAdmin || this.isGestor; + } detailOpen = false; detailLoading = false; @@ -137,7 +159,8 @@ export class Parcelamentos implements OnInit, OnDestroy { constructor( private parcelamentosService: ParcelamentosService, - private authService: AuthService + private authService: AuthService, + private tableExportService: TableExportService ) {} ngOnInit(): void { @@ -147,6 +170,7 @@ export class Parcelamentos implements OnInit, OnDestroy { ngOnDestroy(): void { this.cancelDetailRequest(); + if (this.toastTimer) clearTimeout(this.toastTimer); } @HostListener('document:keydown.escape') @@ -159,30 +183,24 @@ export class Parcelamentos implements OnInit, OnDestroy { private syncPermissions(): void { this.isSysAdmin = this.authService.hasRole('sysadmin'); + this.isGestor = this.authService.hasRole('gestor'); + this.isFinanceiro = this.authService.hasRole('financeiro'); } get totalPages(): number { - return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10))); + return computeTotalPages(this.total || 0, this.pageSize || 10); } get pageNumbers(): number[] { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } get pageStart(): number { - return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + return computePageStart(this.total || 0, this.page, this.pageSize); } get pageEnd(): number { - return this.total === 0 ? 0 : Math.min(this.page * this.pageSize, this.total); + return computePageEnd(this.total || 0, this.page, this.pageSize); } get competenciaInvalid(): boolean { @@ -273,6 +291,50 @@ export class Parcelamentos implements OnInit, OnDestroy { this.load(); } + async onExport(): Promise { + if (this.exporting) return; + this.exporting = true; + + try { + const baseRows = await this.fetchAllItemsForExport(); + const rows = await this.fetchDetailedItemsForExport(baseRows); + if (!rows.length) { + this.showToast('Nenhum registro encontrado para exportar.', 'danger'); + return; + } + + const timestamp = this.tableExportService.buildTimestamp(); + await this.tableExportService.exportAsXlsx({ + fileName: `parcelamentos_${this.activeSegment}_${timestamp}`, + sheetName: 'Parcelamentos', + rows, + columns: [ + { header: 'ID', value: (row) => row.id ?? '' }, + { header: 'Ano Ref', type: 'number', value: (row) => this.toNumber(row.anoRef) ?? 0 }, + { header: 'Item', type: 'number', value: (row) => this.toNumber(row.item) ?? 0 }, + { header: 'Linha', value: (row) => row.linha ?? '' }, + { header: 'Cliente', value: (row) => row.cliente ?? '' }, + { header: 'Status', value: (row) => row.statusLabel }, + { header: 'Parcela Atual', type: 'number', value: (row) => this.toNumber(row.parcelaAtual) ?? 0 }, + { header: 'Total Parcelas', type: 'number', value: (row) => this.toNumber(row.totalParcelas) ?? 0 }, + { header: 'Qt Parcelas', value: (row) => row.qtParcelas ?? '' }, + { header: 'Valor Cheio', type: 'currency', value: (row) => this.toNumber(row.valorCheio) ?? 0 }, + { header: 'Desconto', type: 'currency', value: (row) => this.toNumber(row.desconto) ?? 0 }, + { header: 'Valor c/ Desconto', type: 'currency', value: (row) => this.toNumber(row.valorComDesconto) ?? 0 }, + { header: 'Valor Parcela', type: 'currency', value: (row) => this.toNumber(row.valorParcela) ?? 0 }, + { header: 'Parcelas Mensais', value: (row) => this.stringifyParcelasMensais(row.parcelasMensais) }, + { header: 'Detalhamento Anual', value: (row) => this.stringifyAnnualRows(row.annualRows) }, + ], + }); + + this.showToast(`Planilha exportada com ${rows.length} registro(s).`, 'success'); + } catch { + this.showToast('Erro ao exportar planilha.', 'danger'); + } finally { + this.exporting = false; + } + } + onPageSizeChange(size: number): void { this.pageSize = size; this.page = 1; @@ -280,7 +342,7 @@ export class Parcelamentos implements OnInit, OnDestroy { } goToPage(p: number): void { - this.page = Math.max(1, Math.min(this.totalPages, p)); + this.page = clampPage(p, this.totalPages); this.load(); } @@ -356,6 +418,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } openCreateModal(): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + this.createModel = this.buildCreateModel(); this.createError = ''; this.createOpen = true; @@ -368,6 +435,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } saveNewParcelamento(model: ParcelamentoCreateModel): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + if (this.createSaving) return; this.createSaving = true; this.createError = ''; @@ -386,6 +458,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } openEdit(item: ParcelamentoListItem): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + const id = this.getItemId(item); if (!id) return; this.editOpen = true; @@ -421,6 +498,11 @@ export class Parcelamentos implements OnInit, OnDestroy { } saveEditParcelamento(model: ParcelamentoCreateModel): void { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.', 'danger'); + return; + } + if (this.editSaving || !this.editModel || !this.editId) return; this.editSaving = true; this.editError = ''; @@ -669,7 +751,7 @@ export class Parcelamentos implements OnInit, OnDestroy { } private applySearch(list: ParcelamentoViewItem[], term: string): ParcelamentoViewItem[] { - const search = this.normalizeText(term); + const search = normalizeAccentInsensitive(term); if (!search) return list; return list.filter((item) => { const payload = [ @@ -681,10 +763,129 @@ export class Parcelamentos implements OnInit, OnDestroy { ] .map((v) => (v ?? '').toString()) .join(' '); - return this.normalizeText(payload).includes(search); + return normalizeAccentInsensitive(payload).includes(search); }); } + private async fetchAllItemsForExport(): Promise { + const anoRef = this.parseNumber(this.filters.anoRef); + const competenciaAno = this.parseNumber(this.filters.competenciaAno); + const competenciaMes = this.parseNumber(this.filters.competenciaMes); + const sendCompetencia = competenciaAno !== null && competenciaMes !== null; + + const pageSize = 500; + let page = 1; + let expectedTotal = 0; + const allItems: ParcelamentoListItem[] = []; + + while (page <= 500) { + const response = await firstValueFrom( + this.parcelamentosService.list({ + anoRef: anoRef ?? undefined, + linha: this.filters.linha?.trim() || undefined, + cliente: this.filters.cliente?.trim() || undefined, + competenciaAno: sendCompetencia ? competenciaAno ?? undefined : undefined, + competenciaMes: sendCompetencia ? competenciaMes ?? undefined : undefined, + page, + pageSize, + }) + ); + + const normalized = this.normalizeListResponse(response); + allItems.push(...normalized.items); + expectedTotal = normalized.total; + + if (normalized.items.length === 0) break; + if (normalized.items.length < pageSize) break; + if (expectedTotal > 0 && allItems.length >= expectedTotal) break; + page += 1; + } + + const base = allItems.map((item) => this.toViewItem(item)); + const searched = this.applySearch(base, this.filters.search); + return this.activeSegment === 'todos' + ? searched + : searched.filter((item) => item.status === this.activeSegment); + } + + private async fetchDetailedItemsForExport(rows: ParcelamentoViewItem[]): Promise { + if (!rows.length) return []; + + const detailedRows: ParcelamentoExportRow[] = []; + 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) => { + const id = this.getItemId(row); + if (!id) return row; + try { + const detailRes = await firstValueFrom(this.parcelamentosService.getById(id)); + const detail = this.normalizeDetail(detailRes); + return { + ...row, + ...detail, + }; + } catch { + return row; + } + }) + ); + detailedRows.push(...resolved); + } + + return detailedRows; + } + + private stringifyParcelasMensais(parcelas?: ParcelamentoParcela[] | null): string { + if (!parcelas?.length) return ''; + return parcelas + .map((parcela) => { + const competencia = (parcela.competencia ?? '').toString().trim(); + const valor = this.toNumber(parcela.valor); + const valorFmt = valor === null ? '-' : new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(valor); + return `${competencia || '-'}: ${valorFmt}`; + }) + .join(' | '); + } + + private stringifyAnnualRows(rows?: ParcelamentoAnnualRow[] | null): string { + if (!rows?.length) return ''; + return rows + .map((row) => { + const year = this.parseNumber(row.year); + const total = this.toNumber(row.total); + const totalFmt = total === null ? '-' : new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(total); + + const months = (row.months ?? []) + .map((month) => { + const monthNum = this.parseNumber(month.month); + const monthValue = this.toNumber(month.valor); + const monthLabel = monthNum ? String(monthNum).padStart(2, '0') : '--'; + const monthFmt = monthValue === null ? '-' : new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(monthValue); + return `${monthLabel}:${monthFmt}`; + }) + .join(', '); + + return `${year ?? '----'} (Total ${totalFmt})${months ? ` [${months}]` : ''}`; + }) + .join(' | '); + } + + private normalizeListResponse(response: any): { items: ParcelamentoListItem[]; total: number } { + const anyRes: any = response ?? {}; + const items = Array.isArray(anyRes.items) + ? anyRes.items.filter(Boolean) + : Array.isArray(anyRes.Items) + ? anyRes.Items.filter(Boolean) + : []; + const total = typeof anyRes.total === 'number' + ? anyRes.total + : (typeof anyRes.Total === 'number' ? anyRes.Total : 0); + return { items, total }; + } + private resolveStatus(item: ParcelamentoListItem): ParcelamentoStatus { const total = this.toNumber(item.totalParcelas); const atual = this.toNumber(item.parcelaAtual); @@ -982,13 +1183,12 @@ export class Parcelamentos implements OnInit, OnDestroy { return Number.isNaN(n) ? null : n; } - private normalizeText(value: any): string { - return (value ?? '') - .toString() - .trim() - .toUpperCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); + 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 onlyDigits(value: string): string { diff --git a/src/app/pages/resumo/resumo.html b/src/app/pages/resumo/resumo.html index 6edb6da..bd879df 100644 --- a/src/app/pages/resumo/resumo.html +++ b/src/app/pages/resumo/resumo.html @@ -1,4 +1,14 @@ 
+
+
+
+ LineGestao + +
+
{{ toastMessage }}
+
+
+
@@ -118,9 +128,9 @@ {{ macrophonyCompact ? 'Expandir' : 'Compactar' }} -
@@ -437,9 +447,9 @@ {{ group.compact ? 'Expandir' : 'Compactar' }} - diff --git a/src/app/pages/resumo/resumo.ts b/src/app/pages/resumo/resumo.ts index 3a7706d..7efaf4e 100644 --- a/src/app/pages/resumo/resumo.ts +++ b/src/app/pages/resumo/resumo.ts @@ -31,7 +31,17 @@ import { ReservaPorDdd, ReservaTotal } from '../../services/resumo.service'; +import { TableExportService, type ExportCellType } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; +import { normalizeAccentInsensitive } from '../../utils/text-normalization.util'; +import { buildApiBaseUrl } from '../../utils/api-base.util'; type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva'; @@ -85,6 +95,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,10 +154,10 @@ 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`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); this.initTables(); this.initGroupTables(); // Default chart configuration for Enterprise look @@ -172,6 +187,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 { @@ -636,7 +652,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { toggleMacrophonyGroup(key: string) { if (this.macrophonyOpen.has(key)) this.macrophonyOpen.delete(key); else this.macrophonyOpen.add(key); } openMacrophonyDetail(g: MacrophonyGroup) { this.macrophonyDetailGroup = g; this.macrophonyDetailOpen = true; } closeMacrophonyDetail() { this.macrophonyDetailOpen = false; this.macrophonyDetailGroup = null; } - goToMacrophonyPage(p: number) { this.macrophonyPage = p; this.updateMacrophonyView(); } + goToMacrophonyPage(p: number) { this.macrophonyPage = clampPage(p, this.macrophonyTotalPages); this.updateMacrophonyView(); } onGroupedSearch(g: GroupedTableState, value?: string) { if (typeof value === 'string') g.search = value; @@ -644,25 +660,19 @@ 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; } closeGroupedDetail(g: GroupedTableState) { g.detailOpen = false; g.detailGroup = null; } - getGroupedPageStart(g: GroupedTableState) { return g.filtered.length ? ((g.page - 1) * g.pageSize + 1) : 0; } - getGroupedPageEnd(g: GroupedTableState) { return g.filtered.length ? Math.min(g.page * g.pageSize, g.filtered.length) : 0; } + getGroupedPageStart(g: GroupedTableState) { return computePageStart(g.filtered.length, g.page, g.pageSize); } + getGroupedPageEnd(g: GroupedTableState) { return computePageEnd(g.filtered.length, g.page, g.pageSize); } getGroupedPageNumbers(g: GroupedTableState) { - const total = this.getGroupedTotalPages(g); - if (total <= 1) return [1]; - const current = Math.min(Math.max(g.page, 1), total); - const start = Math.max(1, current - 2); - const end = Math.min(total, start + 4); - const adjustedStart = Math.max(1, end - 4); - return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i); + return buildPageNumbers(g.page, this.getGroupedTotalPages(g)); } - getGroupedTotalPages(g: GroupedTableState) { return Math.max(1, Math.ceil(g.filtered.length / g.pageSize)); } + getGroupedTotalPages(g: GroupedTableState) { return computeTotalPages(g.filtered.length, g.pageSize); } goToGroupedPage(g: GroupedTableState, p: number) { - g.page = Math.min(this.getGroupedTotalPages(g), Math.max(1, p)); + g.page = clampPage(p, this.getGroupedTotalPages(g)); this.updateGroupView(g); } getTableRowClass(_: TableState, __: T) { return false; } @@ -677,6 +687,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'); @@ -1045,12 +1059,12 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { }); this.macrophonyGroups = groups; - const search = this.normalizeText(this.macrophonySearch); + const search = normalizeAccentInsensitive(this.macrophonySearch); this.macrophonyFiltered = !search ? groups : groups.filter((group) => - this.normalizeText(group.plano).includes(search) || - this.normalizeText(group.gbLabel).includes(search) + normalizeAccentInsensitive(group.plano).includes(search) || + normalizeAccentInsensitive(group.gbLabel).includes(search) ); const totalPages = Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize)); @@ -1086,12 +1100,12 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { if (group.groupSort) groups.sort(group.groupSort); group.groups = groups; - const search = this.normalizeText(group.search); + const search = normalizeAccentInsensitive(group.search); group.filtered = !search ? groups : groups.filter((g) => - this.normalizeText(g.title).includes(search) || - this.normalizeText(g.subtitle).includes(search) + normalizeAccentInsensitive(g.title).includes(search) || + normalizeAccentInsensitive(g.subtitle).includes(search) ); const totalPages = Math.max(1, Math.ceil(group.filtered.length / group.pageSize)); @@ -1106,15 +1120,6 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { } } - private normalizeText(value: any): string { - return (value ?? '') - .toString() - .trim() - .toUpperCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); - } - private sumGroup(rows: T[], getter: (row: T) => any): number { return rows.reduce((acc, row) => acc + (this.toNumber(getter(row)) ?? 0), 0); } @@ -1133,7 +1138,7 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { rows.forEach((row) => { const planoContrato = (row.planoContrato ?? '-').toString().trim() || '-'; - const key = this.normalizeText(planoContrato); + const key = normalizeAccentInsensitive(planoContrato); const gb = this.extractGbFromPlanName(planoContrato) ?? this.toNumber(row.gb ?? row.franquiaGb); @@ -1214,78 +1219,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 +1296,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(); @@ -1335,19 +1321,13 @@ export class Resumo implements OnInit, AfterViewInit, OnDestroy { return Array.isArray(this.resumo?.lineTotais) ? (this.resumo?.lineTotais ?? []) : []; } - get macrophonyPageStart() { return (this.macrophonyPage - 1) * this.macrophonyPageSize + 1; } - get macrophonyPageEnd() { return Math.min(this.macrophonyPage * this.macrophonyPageSize, this.macrophonyFilteredGroups.length); } + get macrophonyPageStart() { return computePageStart(this.macrophonyFilteredGroups.length, this.macrophonyPage, this.macrophonyPageSize); } + get macrophonyPageEnd() { return computePageEnd(this.macrophonyFilteredGroups.length, this.macrophonyPage, this.macrophonyPageSize); } get macrophonyFilteredGroups() { return this.macrophonyFiltered; } get macrophonyPageNumbers() { - const total = this.macrophonyTotalPages; - if (total <= 1) return [1]; - const current = Math.min(Math.max(this.macrophonyPage, 1), total); - const start = Math.max(1, current - 2); - const end = Math.min(total, start + 4); - const adjustedStart = Math.max(1, end - 4); - return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i); + return buildPageNumbers(this.macrophonyPage, this.macrophonyTotalPages); } - get macrophonyTotalPages() { return Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize)); } + get macrophonyTotalPages() { return computeTotalPages(this.macrophonyFiltered.length, this.macrophonyPageSize); } get planosTotals() { return this.resumo?.macrophonyTotals; } get contratosTotals() { return this.resumo?.planoContratoTotal; } get clientesTotals() { return this.resumo?.vivoLineTotals; } diff --git a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts index a52be9f..978f34a 100644 --- a/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts +++ b/src/app/pages/solicitacoes-linhas/solicitacoes-linhas.ts @@ -3,6 +3,13 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; import { SolicitacaoLinhaDto, SolicitacoesLinhasService } from '../../services/solicitacoes-linhas.service'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; @Component({ selector: 'app-solicitacoes-linhas', @@ -64,34 +71,24 @@ export class SolicitacoesLinhas implements OnInit, OnDestroy { } goToPage(pageNumber: number): void { - this.page = Math.max(1, Math.min(this.totalPages, pageNumber)); + this.page = clampPage(pageNumber, this.totalPages); this.fetch(); } get totalPages(): number { - return Math.ceil((this.total || 0) / this.pageSize) || 1; + return computeTotalPages(this.total || 0, this.pageSize); } get pageNumbers(): number[] { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } get pageStart(): number { - return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; + return computePageStart(this.total || 0, this.page, this.pageSize); } get pageEnd(): number { - if (this.total === 0) return 0; - return Math.min(this.page * this.pageSize, this.total); + return computePageEnd(this.total || 0, this.page, this.pageSize); } private parseDate(value?: string | null): Date | null { diff --git a/src/app/pages/troca-numero/troca-numero.html b/src/app/pages/troca-numero/troca-numero.html index 6e31ffb..c8fa43f 100644 --- a/src/app/pages/troca-numero/troca-numero.html +++ b/src/app/pages/troca-numero/troca-numero.html @@ -31,7 +31,11 @@
- +
@@ -86,7 +90,6 @@ Itens por pág:
-
@@ -153,7 +156,7 @@ {{ r.observacao || '-' }}
-
@@ -194,184 +197,4 @@
- - - - - - - - - + diff --git a/src/app/pages/troca-numero/troca-numero.scss b/src/app/pages/troca-numero/troca-numero.scss index 8c6fa2e..4e94435 100644 --- a/src/app/pages/troca-numero/troca-numero.scss +++ b/src/app/pages/troca-numero/troca-numero.scss @@ -534,9 +534,6 @@ } /* MODALS */ -.modal-backdrop-custom { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 9990; backdrop-filter: blur(4px); } -.modal-custom { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } - .modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.8); diff --git a/src/app/pages/troca-numero/troca-numero.ts b/src/app/pages/troca-numero/troca-numero.ts index b27b637..7264919 100644 --- a/src/app/pages/troca-numero/troca-numero.ts +++ b/src/app/pages/troca-numero/troca-numero.ts @@ -10,8 +10,20 @@ import { import { isPlatformBrowser, CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient, HttpParams } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { AuthService } from '../../services/auth.service'; import { CustomSelectComponent } from '../../components/custom-select/custom-select'; +import { TableExportService } from '../../services/table-export.service'; import { environment } from '../../../environments/environment'; +import { TrocaNumeroModalsComponent } from '../../components/page-modals/troca-numero-modals/troca-numero-modals'; +import { + buildPageNumbers, + clampPage, + computePageEnd, + computePageStart, + computeTotalPages +} from '../../utils/pagination.util'; +import { buildApiEndpoint } from '../../utils/api-base.util'; type TrocaKey = 'item' | 'linhaAntiga' | 'linhaNova' | 'iccid' | 'dataTroca' | 'motivo' | 'observacao'; @@ -56,34 +68,30 @@ interface LineOptionDto { @Component({ standalone: true, - imports: [CommonModule, FormsModule, CustomSelectComponent], + imports: [CommonModule, FormsModule, CustomSelectComponent, TrocaNumeroModalsComponent], templateUrl: './troca-numero.html', styleUrls: ['./troca-numero.scss'] }) export class TrocaNumero implements AfterViewInit { + readonly vm = this; 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 authService: AuthService, + private tableExportService: TableExportService ) {} - private readonly apiBase = (() => { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; - return `${apiBase}/trocanumero`; - })(); + private readonly apiBase = buildApiEndpoint(environment.apiUrl, 'trocanumero'); /** ✅ base do GERAL (para buscar clientes/linhas no modal) */ - private readonly linesApiBase = (() => { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; - return `${apiBase}/lines`; - })(); + private readonly linesApiBase = buildApiEndpoint(environment.apiUrl, 'lines'); // ====== DATA ====== groups: GroupItem[] = []; @@ -132,9 +140,20 @@ export class TrocaNumero implements AfterViewInit { loadingClients = false; loadingLines = false; + isSysAdmin = false; + isGestor = false; + isFinanceiro = false; + + get canManageRecords(): boolean { + return this.isSysAdmin || this.isGestor; + } + async ngAfterViewInit() { if (!isPlatformBrowser(this.platformId)) return; this.initAnimations(); + this.isSysAdmin = this.authService.hasRole('sysadmin'); + this.isGestor = this.authService.hasRole('gestor'); + this.isFinanceiro = this.authService.hasRole('financeiro'); setTimeout(() => this.refresh()); } @@ -151,6 +170,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(() => { @@ -175,29 +278,19 @@ export class TrocaNumero implements AfterViewInit { } goToPage(p: number) { - this.page = Math.max(1, Math.min(this.totalPages, p)); + this.page = clampPage(p, this.totalPages); this.applyPagination(); } - get totalPages() { return Math.ceil((this.total || 0) / this.pageSize) || 1; } + get totalPages() { return computeTotalPages(this.total || 0, this.pageSize); } get pageNumbers() { - const total = this.totalPages; - const current = this.page; - const max = 5; - let start = Math.max(1, current - 2); - let end = Math.min(total, start + (max - 1)); - start = Math.max(1, end - (max - 1)); - - const pages: number[] = []; - for (let i = start; i <= end; i++) pages.push(i); - return pages; + return buildPageNumbers(this.page, this.totalPages); } - get pageStart() { return this.total === 0 ? 0 : (this.page - 1) * this.pageSize + 1; } + get pageStart() { return computePageStart(this.total || 0, this.page, this.pageSize); } get pageEnd() { - if (this.total === 0) return 0; - return Math.min(this.page * this.pageSize, this.total); + return computePageEnd(this.total || 0, this.page, this.pageSize); } trackById(_: number, row: TrocaRow) { return row.id; } @@ -414,6 +507,11 @@ export class TrocaNumero implements AfterViewInit { // ====== MODAL EDIÇÃO ====== onEditar(r: TrocaRow) { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.editOpen = true; this.editSaving = false; @@ -436,6 +534,11 @@ export class TrocaNumero implements AfterViewInit { } saveEdit() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + if (!this.editModel || !this.editModel.id) return; this.editSaving = true; @@ -467,6 +570,11 @@ export class TrocaNumero implements AfterViewInit { // ====== MODAL CRIAÇÃO ====== onCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + this.createOpen = true; this.createSaving = false; @@ -496,6 +604,11 @@ export class TrocaNumero implements AfterViewInit { } saveCreate() { + if (!this.canManageRecords) { + this.showToast('Perfil Financeiro possui acesso somente leitura.'); + return; + } + // ✅ validações do "beber do GERAL" if (!String(this.selectedCliente ?? '').trim()) { this.showToast('Selecione um Cliente do GERAL.'); @@ -542,6 +655,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..fa2b8bf 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
+ @@ -188,268 +192,4 @@
-
- -
- -
- - -
- -
- - -
- -
- - -
- -
- + diff --git a/src/app/pages/vigencia/vigencia.scss b/src/app/pages/vigencia/vigencia.scss index 5e41891..c28c00a 100644 --- a/src/app/pages/vigencia/vigencia.scss +++ b/src/app/pages/vigencia/vigencia.scss @@ -372,14 +372,6 @@ .pagination-modern .page-item.active .page-link { background-color: var(--blue); border-color: var(--blue); color: #fff; } /* MODAL */ -.lg-backdrop { - position: fixed; - inset: 0; - background: radial-gradient(circle at 20% 0%, rgba(227, 61, 207, 0.18), rgba(0, 0, 0, 0.55) 45%); - z-index: 9990; - backdrop-filter: blur(5px); -} -.lg-modal { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 9995; padding: 16px; } .lg-modal-card { background: #ffffff; border: 1px solid rgba(255,255,255,0.86); diff --git a/src/app/pages/vigencia/vigencia.ts b/src/app/pages/vigencia/vigencia.ts index 31d1148..0bcd584 100644 --- a/src/app/pages/vigencia/vigencia.ts +++ b/src/app/pages/vigencia/vigencia.ts @@ -3,13 +3,16 @@ 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'; +import { computeTotalPages } from '../../utils/pagination.util'; +import { VigenciaModalsComponent } from '../../components/page-modals/vigencia-modals/vigencia-modals'; type SortDir = 'asc' | 'desc'; type ToastType = 'success' | 'danger'; @@ -26,12 +29,14 @@ interface LineOptionDto { @Component({ selector: 'app-vigencia', standalone: true, - imports: [CommonModule, FormsModule, CustomSelectComponent], + imports: [CommonModule, FormsModule, CustomSelectComponent, VigenciaModalsComponent], templateUrl: './vigencia.html', styleUrls: ['./vigencia.scss'], }) export class VigenciaComponent implements OnInit, OnDestroy { + readonly vm = this; loading = false; + exporting = false; errorMsg = ''; // Filtros @@ -113,7 +118,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 { @@ -157,7 +163,7 @@ export class VigenciaComponent implements OnInit, OnDestroy { } get totalPages(): number { - return Math.max(1, Math.ceil((this.total || 0) / (this.pageSize || 10))); + return computeTotalPages(this.total || 0, this.pageSize || 10); } fetch(goToPage?: number): void { @@ -295,6 +301,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/billing.ts b/src/app/services/billing.ts index a03c960..a3070bc 100644 --- a/src/app/services/billing.ts +++ b/src/app/services/billing.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable, map } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type SortDir = 'asc' | 'desc'; export type TipoCliente = 'PF' | 'PJ'; @@ -75,8 +76,7 @@ export class BillingService { private readonly baseUrl: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + const apiBase = buildApiBaseUrl(environment.apiUrl); this.baseUrl = `${apiBase}/billing`; } diff --git a/src/app/services/chips-controle.service.ts b/src/app/services/chips-controle.service.ts index 16423a7..2afafb4 100644 --- a/src/app/services/chips-controle.service.ts +++ b/src/app/services/chips-controle.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type SortDir = 'asc' | 'desc'; @@ -71,8 +72,7 @@ export class ChipsControleService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } getChipsVirgens(opts: { diff --git a/src/app/services/dados-usuarios.service.ts b/src/app/services/dados-usuarios.service.ts index 0e78ddc..cd3c8d0 100644 --- a/src/app/services/dados-usuarios.service.ts +++ b/src/app/services/dados-usuarios.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type SortDir = 'asc' | 'desc'; @@ -75,8 +76,7 @@ export class DadosUsuariosService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } getGroups(opts: { diff --git a/src/app/services/historico.service.ts b/src/app/services/historico.service.ts index 2fb0c5c..bab885e 100644 --- a/src/app/services/historico.service.ts +++ b/src/app/services/historico.service.ts @@ -3,6 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type AuditAction = 'CREATE' | 'UPDATE' | 'DELETE'; export type AuditChangeType = 'added' | 'modified' | 'removed'; @@ -50,13 +51,24 @@ export interface HistoricoQuery { pageSize?: number; } +export interface LineHistoricoQuery { + line: string; + pageName?: string; + action?: AuditAction | string; + user?: string; + search?: string; + dateFrom?: string; + dateTo?: string; + page?: number; + pageSize?: number; +} + @Injectable({ providedIn: 'root' }) export class HistoricoService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } list(params: HistoricoQuery): Observable> { @@ -74,4 +86,20 @@ export class HistoricoService { return this.http.get>(`${this.baseApi}/historico`, { params: httpParams }); } + + listByLine(params: LineHistoricoQuery): Observable> { + let httpParams = new HttpParams(); + if (params.line) httpParams = httpParams.set('line', params.line); + if (params.pageName) httpParams = httpParams.set('pageName', params.pageName); + if (params.action) httpParams = httpParams.set('action', params.action); + if (params.user) httpParams = httpParams.set('user', params.user); + if (params.search) httpParams = httpParams.set('search', params.search); + if (params.dateFrom) httpParams = httpParams.set('dateFrom', params.dateFrom); + if (params.dateTo) httpParams = httpParams.set('dateTo', params.dateTo); + + httpParams = httpParams.set('page', String(params.page || 1)); + httpParams = httpParams.set('pageSize', String(params.pageSize || 10)); + + return this.http.get>(`${this.baseApi}/historico/linhas`, { params: httpParams }); + } } diff --git a/src/app/services/lines.service.ts b/src/app/services/lines.service.ts index 022570c..5b18d45 100644 --- a/src/app/services/lines.service.ts +++ b/src/app/services/lines.service.ts @@ -2,6 +2,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export interface PagedResult { page: number; @@ -72,8 +73,7 @@ export class LinesService { private readonly baseUrl: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + const apiBase = buildApiBaseUrl(environment.apiUrl); this.baseUrl = `${apiBase}/lines`; } diff --git a/src/app/services/notifications.service.ts b/src/app/services/notifications.service.ts index b78119d..6db4393 100644 --- a/src/app/services/notifications.service.ts +++ b/src/app/services/notifications.service.ts @@ -3,6 +3,7 @@ import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { Observable, Subject, tap } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type NotificationTipo = 'AVencer' | 'Vencido' | 'RenovacaoAutomatica' | string; @@ -40,8 +41,7 @@ export class NotificationsService { readonly events$ = this.eventsSubject.asObservable(); constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } list(): Observable { diff --git a/src/app/services/parcelamentos.service.ts b/src/app/services/parcelamentos.service.ts index 1a3a9b2..484c034 100644 --- a/src/app/services/parcelamentos.service.ts +++ b/src/app/services/parcelamentos.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export interface PagedResult { page: number; @@ -76,8 +77,7 @@ export class ParcelamentosService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } list(filters: { diff --git a/src/app/services/profile.service.ts b/src/app/services/profile.service.ts index 3879844..f6ec137 100644 --- a/src/app/services/profile.service.ts +++ b/src/app/services/profile.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type ProfileMeDto = { id: string; @@ -26,8 +27,7 @@ export class ProfileService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } getMe(): Observable { diff --git a/src/app/services/resumo.service.ts b/src/app/services/resumo.service.ts index 507872f..011dd6e 100644 --- a/src/app/services/resumo.service.ts +++ b/src/app/services/resumo.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export interface MacrophonyPlan { planoContrato?: string | null; @@ -119,8 +120,7 @@ export class ResumoService { private readonly apiBase: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.apiBase = buildApiBaseUrl(environment.apiUrl); } getResumo() { diff --git a/src/app/services/solicitacoes-linhas.service.ts b/src/app/services/solicitacoes-linhas.service.ts index d3982ff..228e5de 100644 --- a/src/app/services/solicitacoes-linhas.service.ts +++ b/src/app/services/solicitacoes-linhas.service.ts @@ -2,6 +2,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export interface PagedResult { page: number; @@ -37,8 +38,7 @@ export class SolicitacoesLinhasService { private readonly baseUrl: string; constructor(private readonly http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + const apiBase = buildApiBaseUrl(environment.apiUrl); this.baseUrl = `${apiBase}/solicitacoes-linhas`; } diff --git a/src/app/services/sysadmin.service.ts b/src/app/services/sysadmin.service.ts index b59eefa..4b01ab6 100644 --- a/src/app/services/sysadmin.service.ts +++ b/src/app/services/sysadmin.service.ts @@ -3,6 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type SystemTenantDto = { tenantId: string; @@ -34,8 +35,7 @@ export class SysadminService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } listTenants(params?: ListSystemTenantsParams): Observable { diff --git a/src/app/services/table-export.service.ts b/src/app/services/table-export.service.ts new file mode 100644 index 0000000..8c9f72d --- /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'; +import { buildApiBaseUrl } from '../utils/api-base.util'; + +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 apiBase = buildApiBaseUrl(environment.apiUrl); + 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; + } + } +} diff --git a/src/app/services/users.service.ts b/src/app/services/users.service.ts index 115f3ce..8dd2c9d 100644 --- a/src/app/services/users.service.ts +++ b/src/app/services/users.service.ts @@ -3,8 +3,9 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; -export type UserPermission = 'sysadmin' | 'gestor' | 'cliente'; +export type UserPermission = 'sysadmin' | 'gestor' | 'financeiro' | 'cliente'; export type UserDto = { id: string; @@ -60,8 +61,7 @@ export class UsersService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } create(payload: CreateUserPayload): Observable { diff --git a/src/app/services/vigencia.service.ts b/src/app/services/vigencia.service.ts index eb65938..c6bcb54 100644 --- a/src/app/services/vigencia.service.ts +++ b/src/app/services/vigencia.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '../../environments/environment'; +import { buildApiBaseUrl } from '../utils/api-base.util'; export type SortDir = 'asc' | 'desc'; @@ -76,8 +77,7 @@ export class VigenciaService { private readonly baseApi: string; constructor(private http: HttpClient) { - const raw = (environment.apiUrl || '').replace(/\/+$/, ''); - this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; + this.baseApi = buildApiBaseUrl(environment.apiUrl); } getVigencia(opts: { search?: string; client?: string; page?: number; pageSize?: number; sortBy?: string; sortDir?: SortDir; }): Observable> { diff --git a/src/app/utils/api-base.util.ts b/src/app/utils/api-base.util.ts new file mode 100644 index 0000000..b5269ff --- /dev/null +++ b/src/app/utils/api-base.util.ts @@ -0,0 +1,11 @@ +export function buildApiBaseUrl(apiUrl: string | null | undefined): string { + const raw = (apiUrl ?? '').toString().trim().replace(/\/+$/, ''); + if (!raw) return '/api'; + return raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`; +} + +export function buildApiEndpoint(apiUrl: string | null | undefined, resourcePath: string): string { + const base = buildApiBaseUrl(apiUrl); + const cleanedPath = (resourcePath ?? '').toString().trim().replace(/^\/+/, ''); + return cleanedPath ? `${base}/${cleanedPath}` : base; +} diff --git a/src/app/utils/pagination.util.ts b/src/app/utils/pagination.util.ts new file mode 100644 index 0000000..3e49a16 --- /dev/null +++ b/src/app/utils/pagination.util.ts @@ -0,0 +1,35 @@ +export function computeTotalPages(total: number, pageSize: number): number { + const safeTotal = Number.isFinite(total) ? Math.max(0, total) : 0; + const safeSize = Number.isFinite(pageSize) ? Math.max(1, pageSize) : 1; + return Math.max(1, Math.ceil(safeTotal / safeSize)); +} + +export function clampPage(page: number, totalPages: number): number { + return Math.max(1, Math.min(totalPages, page)); +} + +export function buildPageNumbers(currentPage: number, totalPages: number, maxVisible = 5): number[] { + const safeTotal = Math.max(1, totalPages); + const safeCurrent = clampPage(currentPage, safeTotal); + const safeMax = Math.max(1, maxVisible); + + let start = Math.max(1, safeCurrent - Math.floor(safeMax / 2)); + let end = Math.min(safeTotal, start + (safeMax - 1)); + start = Math.max(1, end - (safeMax - 1)); + + const pages: number[] = []; + for (let i = start; i <= end; i += 1) pages.push(i); + return pages; +} + +export function computePageStart(total: number, page: number, pageSize: number): number { + if (!Number.isFinite(total) || total <= 0) return 0; + return (clampPage(page, computeTotalPages(total, pageSize)) - 1) * Math.max(1, pageSize) + 1; +} + +export function computePageEnd(total: number, page: number, pageSize: number): number { + if (!Number.isFinite(total) || total <= 0) return 0; + const safeSize = Math.max(1, pageSize); + const safePage = clampPage(page, computeTotalPages(total, safeSize)); + return Math.min(safePage * safeSize, total); +} diff --git a/src/app/utils/text-normalization.util.ts b/src/app/utils/text-normalization.util.ts new file mode 100644 index 0000000..31a1ff3 --- /dev/null +++ b/src/app/utils/text-normalization.util.ts @@ -0,0 +1,16 @@ +export type NormalizeCaseMode = 'upper' | 'lower' | 'none'; + +export function normalizeAccentInsensitive( + value: unknown, + caseMode: NormalizeCaseMode = 'upper' +): string { + const normalized = (value ?? '') + .toString() + .trim() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + + if (caseMode === 'lower') return normalized.toLowerCase(); + if (caseMode === 'none') return normalized; + return normalized.toUpperCase(); +} diff --git a/tsconfig.app.json b/tsconfig.app.json index ef19921..a2b81e4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,6 +4,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", + "noPropertyAccessFromIndexSignature": false, "types": [ "node" ]