diff --git a/package-lock.json b/package-lock.json
index f8954cd..ef276f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,9 +11,39 @@
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.7.0",
"dotenv": "^16.4.7",
+ "exceljs": "^4.4.0",
"express": "^4.21.2",
"express-session": "^1.18.1",
- "mysql2": "^3.12.0"
+ "mysql2": "^3.12.0",
+ "node-cron": "^4.2.1",
+ "nodemailer": "^8.0.1"
+ }
+ },
+ "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==",
+ "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/parse": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz",
+ "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==",
+ "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/@mapbox/node-pre-gyp": {
@@ -36,6 +66,11 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
+ "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=="
+ },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -105,6 +140,70 @@
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
"license": "ISC"
},
+ "node_modules/archiver": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
+ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
+ "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==",
+ "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==",
+ "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=="
+ },
+ "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==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
@@ -125,6 +224,11 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
+ },
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
@@ -185,6 +289,26 @@
"prebuild-install": "^7.1.1"
}
},
+ "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==",
+ "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==",
+ "dependencies": {
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@@ -205,6 +329,11 @@
"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=="
+ },
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@@ -263,6 +392,30 @@
"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==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-indexof-polyfill": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
+ "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
+ "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",
@@ -301,6 +454,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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==",
+ "dependencies": {
+ "traverse": ">=0.3.0 <0.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -319,6 +483,20 @@
"color-support": "bin.js"
}
},
+ "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==",
+ "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",
@@ -367,6 +545,39 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
+ "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=="
+ },
+ "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==",
+ "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==",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -469,6 +680,41 @@
"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==",
+ "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==",
+ "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=="
+ },
+ "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==",
+ "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",
@@ -544,6 +790,25 @@
"node": ">= 0.6"
}
},
+ "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==",
+ "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/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -622,6 +887,18 @@
"url": "https://opencollective.com/express"
}
},
+ "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==",
+ "dependencies": {
+ "@fast-csv/format": "4.3.5",
+ "@fast-csv/parse": "4.3.6"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -700,6 +977,44 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
+ "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.",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ },
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/fstream/node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "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",
+ "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",
@@ -815,6 +1130,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/graceful-fs": {
+ "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=="
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -933,6 +1253,11 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -980,6 +1305,166 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "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==",
+ "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=="
+ },
+ "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==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "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==",
+ "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=="
+ },
+ "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==",
+ "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==",
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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."
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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=="
+ },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -1266,6 +1751,14 @@
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
+ "node_modules/node-cron": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
+ "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -1286,6 +1779,14 @@
}
}
},
+ "node_modules/nodemailer": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
+ "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -1301,6 +1802,14 @@
"node": ">=6"
}
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
@@ -1365,6 +1874,11 @@
"wrappy": "1"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1415,6 +1929,11 @@
"node": ">=10"
}
},
+ "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=="
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1515,6 +2034,33 @@
"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==",
+ "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==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -1557,6 +2103,17 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
+ "node_modules/saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
@@ -1625,6 +2182,11 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
+ "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=="
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -1868,6 +2430,14 @@
"node": ">=6"
}
},
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1883,6 +2453,14 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/traverse": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+ "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -1929,6 +2507,50 @@
"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==",
+ "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==",
+ "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=="
+ },
+ "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==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -1944,6 +2566,14 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -1984,11 +2614,49 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
+ },
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
+ },
+ "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==",
+ "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==",
+ "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"
+ }
}
}
}
diff --git a/package.json b/package.json
index 87c5dfd..25c8684 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,11 @@
"bcrypt": "^5.1.1",
"better-sqlite3": "^11.7.0",
"dotenv": "^16.4.7",
+ "exceljs": "^4.4.0",
"express": "^4.21.2",
"express-session": "^1.18.1",
- "mysql2": "^3.12.0"
+ "mysql2": "^3.12.0",
+ "node-cron": "^4.2.1",
+ "nodemailer": "^8.0.1"
}
}
diff --git a/server.js b/server.js
index 80ba1b8..4bec7c4 100644
--- a/server.js
+++ b/server.js
@@ -11,13 +11,20 @@ const express = require('express');
const session = require('express-session');
const path = require('path');
const { authenticate, requireAuth, requireRole, createAgente, createUser } = require('./src/auth');
-const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData, fetchMerchantProfile, fetchMerchantData } = require('./src/queries');
+const { fetchTransacoes, fetchAllTransacoes, serialize, fetchDailyStats, fetchKPIs, fetchTrend30Days, fetchTopAgentes, fetchTrendByPeriod, fetchKPIsByPeriod, fetchBIData, fetchRevenueAnalytics, fetchBIStrategic, fetchTopClients, fetchClientSearch, fetchClientProfile, fetchClientData, fetchMerchantProfile, fetchMerchantData, fetchProviderPerformance, fetchFailedTransactions, fetchProviderTrend } = require('./src/queries');
+const { buildAdminProvidersHTML } = require('./src/admin-providers');
+const pool = require('./src/db-rds');
const { buildHTML } = require('./src/dashboard');
const { buildAdminHTML } = require('./src/admin-panel');
const { buildAdminHomeHTML } = require('./src/admin-home');
const { buildAdminDashboardHTML } = require('./src/admin-dashboard');
const { buildAdminBIHTML } = require('./src/admin-bi');
const { buildAdminClienteHTML } = require('./src/admin-cliente');
+const { exportToExcel, createWorkbook, sendWorkbook } = require('./src/export/excel-export');
+const { startAlertEngine, getAlerts, acknowledgeAlert, getAlertHistory, getUnackedCount } = require('./src/alerts/alert-engine');
+const { predictChurnRisk } = require('./src/services/churn-predictor');
+const { forecastFromTrend } = require('./src/services/forecast');
+const { startETL } = require('./src/etl/daily-sync');
const bcrypt = require('bcrypt');
const db = require('./src/db-local');
const cache = require('./src/cache');
@@ -580,6 +587,368 @@ app.delete('/admin/agentes/:id', requireRole('admin'), (req, res) => {
}
});
+// --- Excel Export Endpoints ---
+
+app.get('/admin/api/export/bi-excel', requireRole('admin'), async (req, res) => {
+ try {
+ const { start, end } = req.query;
+ if (!start || !end) return res.status(400).json({ error: 'start and end required' });
+ const getAgenteName = (agenteId) => {
+ const row = db.prepare('SELECT nome FROM agentes WHERE agente_id = ?').get(agenteId);
+ return row ? row.nome : null;
+ };
+ const data = await fetchBIData(start, end, getAgenteName);
+
+ // Build multi-sheet workbook
+ const ExcelJS = require('exceljs');
+ const workbook = new ExcelJS.Workbook();
+ workbook.creator = 'CambioReal BI-CCC';
+
+ // Sheet 1: KPI Summary
+ const kpiSheet = workbook.addWorksheet('KPI Summary');
+ kpiSheet.columns = [
+ { header: 'Metric', key: 'metric', width: 30 },
+ { header: 'Current', key: 'current', width: 18 },
+ { header: 'Previous', key: 'previous', width: 18 },
+ { header: 'Change %', key: 'change', width: 14 }
+ ];
+ const k = data.kpis.total;
+ const c = data.comparison;
+ const pctChg = (curr, prev) => prev > 0 ? Math.round((curr - prev) / prev * 100) : 0;
+ kpiSheet.addRows([
+ { metric: 'Total Transactions', current: k.qtd, previous: c.prev_qtd, change: pctChg(k.qtd, c.prev_qtd) },
+ { metric: 'Total Volume USD', current: k.vol_usd, previous: c.prev_vol_usd, change: pctChg(k.vol_usd, c.prev_vol_usd) },
+ { metric: 'Spread Revenue', current: k.spread_revenue, previous: c.prev_spread, change: pctChg(k.spread_revenue, c.prev_spread) },
+ { metric: 'Active Clients', current: k.clientes, previous: '-', change: '-' },
+ { metric: 'Avg Ticket', current: k.ticket_medio, previous: '-', change: '-' },
+ { metric: 'Retention Rate %', current: data.retention.rate, previous: '-', change: '-' }
+ ]);
+
+ // Sheet 2: Top Clients
+ const clientSheet = workbook.addWorksheet('Top Clients');
+ clientSheet.columns = [
+ { header: 'Client', key: 'nome', width: 30 },
+ { header: 'Volume USD', key: 'vol_usd', width: 18 },
+ { header: 'Transactions', key: 'qtd', width: 14 }
+ ];
+ data.topClients.forEach(c => clientSheet.addRow(c));
+
+ // Sheet 3: Agent Ranking
+ const agentSheet = workbook.addWorksheet('Agent Ranking');
+ agentSheet.columns = [
+ { header: 'Rank', key: 'rank', width: 8 },
+ { header: 'Agent', key: 'nome', width: 25 },
+ { header: 'Volume USD', key: 'vol_usd', width: 18 },
+ { header: 'Transactions', key: 'qtd', width: 14 },
+ { header: 'Spread Revenue', key: 'spread_revenue', width: 18 },
+ { header: 'Clients', key: 'clientes', width: 12 }
+ ];
+ data.agentRanking.forEach(a => agentSheet.addRow(a));
+
+ // Sheet 4: Clients at Risk
+ const riskSheet = workbook.addWorksheet('Clients at Risk');
+ riskSheet.columns = [
+ { header: 'Client', key: 'nome', width: 30 },
+ { header: 'Volume USD', key: 'vol_usd', width: 18 },
+ { header: 'Transactions', key: 'qtd', width: 14 },
+ { header: 'Last Activity', key: 'last_op', width: 18 },
+ { header: 'Days Inactive', key: 'days_inactive', width: 14 }
+ ];
+ data.clientsAtRisk.forEach(c => riskSheet.addRow(c));
+
+ // Style all sheet headers
+ workbook.eachSheet(sheet => {
+ const hr = sheet.getRow(1);
+ hr.eachCell(cell => {
+ cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF7600BE' } };
+ cell.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 };
+ cell.alignment = { vertical: 'middle', horizontal: 'center' };
+ });
+ hr.height = 28;
+ sheet.views = [{ state: 'frozen', ySplit: 1 }];
+ });
+
+ await sendWorkbook(res, workbook, `BI_Executive_${start}_${end}`);
+ } catch (err) {
+ console.error('BI Export error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/admin/api/export/clients-excel', requireRole('admin'), async (req, res) => {
+ try {
+ const clients = await cache.getOrFetch('top-clients', fetchTopClients, 15 * 60 * 1000);
+ await exportToExcel(res, clients, [
+ { header: 'Client', key: 'nome', width: 30 },
+ { header: 'Volume USD', key: 'vol', width: 18, type: 'currency' },
+ { header: 'Operations', key: 'ops', width: 14, type: 'number' },
+ { header: 'Months Active', key: 'months', width: 14, type: 'number' },
+ { header: 'Last Activity', key: 'lastOp', width: 16, type: 'date' }
+ ], 'Top Clients', `Top_Clients_${new Date().toISOString().slice(0, 10)}`);
+ } catch (err) {
+ console.error('Clients Export error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/admin/api/export/providers-excel', requireRole('admin'), async (req, res) => {
+ try {
+ const { start, end } = req.query;
+ if (!start || !end) return res.status(400).json({ error: 'start and end required' });
+ const data = await fetchProviderPerformance(start, end);
+ await exportToExcel(res, data.providers, [
+ { header: 'Provider', key: 'provider', width: 18 },
+ { header: 'Flow', key: 'flow', width: 12 },
+ { header: 'Total Tx', key: 'total_tx', width: 12, type: 'number' },
+ { header: 'Success Tx', key: 'success_tx', width: 12, type: 'number' },
+ { header: 'Success Rate %', key: 'success_rate', width: 14, type: 'percentage' },
+ { header: 'Volume USD', key: 'vol_usd', width: 18, type: 'currency' },
+ { header: 'Avg Ticket', key: 'avg_ticket', width: 14, type: 'currency' },
+ { header: 'Spread %', key: 'avg_spread_pct', width: 12, type: 'percentage' },
+ { header: 'Settlement Hours', key: 'avg_settlement_hours', width: 16, type: 'number' }
+ ], 'Providers', `Providers_${start}_${end}`);
+ } catch (err) {
+ console.error('Providers Export error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/admin/api/export/transactions-excel', requireRole('admin'), async (req, res) => {
+ try {
+ const { start, end } = req.query;
+ const dias = start && end ? null : 90;
+ let data;
+ if (start && end) {
+ // Use fetchBIData for date-filtered transactions
+ const biData = await fetchBIData(start, end);
+ // Combine trend data into flat rows
+ const allTrend = [];
+ ['brlUsd', 'usdBrl', 'usdUsd'].forEach(flow => {
+ (biData.trend[flow] || []).forEach(r => {
+ allTrend.push({ date: r.dia, flow, transactions: r.qtd, volume_usd: r.vol_usd, avg_spread: r.avg_spread || 0 });
+ });
+ });
+ data = allTrend;
+ } else {
+ const raw = await fetchAllTransacoes(dias || 90);
+ data = serialize(raw.rowsBrlUsd, raw.rowsUsdBrl);
+ }
+
+ await exportToExcel(res, data, data.length > 0 && data[0].fluxo ? [
+ { header: 'Flow', key: 'fluxo', width: 12 },
+ { header: 'Client', key: 'cliente', width: 28 },
+ { header: 'Date', key: 'data_operacao', width: 18 },
+ { header: 'BRL', key: 'valor_reais', width: 14, type: 'currency' },
+ { header: 'USD', key: 'valor_dolar', width: 14, type: 'currency' },
+ { header: 'PTAX', key: 'taxa_ptax', width: 12 },
+ { header: 'Rate', key: 'taxa_cobrada', width: 12 },
+ { header: 'Spread', key: 'spread_bruto', width: 12 },
+ { header: 'Spread %', key: 'spread_pct', width: 10, type: 'percentage' },
+ { header: 'IOF %', key: 'iof_pct', width: 8 },
+ { header: 'Status', key: 'status', width: 14 }
+ ] : [
+ { header: 'Date', key: 'date', width: 14 },
+ { header: 'Flow', key: 'flow', width: 12 },
+ { header: 'Transactions', key: 'transactions', width: 14, type: 'number' },
+ { header: 'Volume USD', key: 'volume_usd', width: 16, type: 'currency' },
+ { header: 'Avg Spread %', key: 'avg_spread', width: 14, type: 'percentage' }
+ ], 'Transactions', `Transactions_${start || 'last90d'}_${end || 'today'}`);
+ } catch (err) {
+ console.error('Transactions Export error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- Forecast API ---
+app.get('/admin/api/bi/forecast', requireRole('admin'), async (req, res) => {
+ try {
+ const metric = req.query.metric || 'volume';
+ const days = parseInt(req.query.days) || 30;
+ // Get last 90 days of trend data for forecasting
+ const now = new Date();
+ const start = new Date(now.getTime() - 90 * 86400000).toISOString().slice(0, 10);
+ const end = now.toISOString().slice(0, 10);
+ const biData = await fetchBIData(start, end);
+
+ // Combine all flows into daily totals
+ const dailyMap = {};
+ ['brlUsd', 'usdBrl', 'usdUsd'].forEach(flow => {
+ (biData.trend[flow] || []).forEach(d => {
+ if (!dailyMap[d.dia]) dailyMap[d.dia] = { dia: d.dia, vol_usd: 0, qtd: 0 };
+ dailyMap[d.dia].vol_usd += d.vol_usd;
+ dailyMap[d.dia].qtd += d.qtd;
+ });
+ });
+ const trendData = Object.values(dailyMap).sort((a, b) => a.dia.localeCompare(b.dia));
+
+ const metricKey = metric === 'transactions' ? 'qtd' : 'vol_usd';
+ const result = forecastFromTrend(trendData, metricKey, days);
+ res.json(result);
+ } catch (err) {
+ console.error('Forecast API error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- Churn Risk API ---
+app.get('/admin/api/cliente/:id/churn', requireRole('admin'), async (req, res) => {
+ try {
+ const clienteId = parseInt(req.params.id);
+ if (!clienteId) return res.status(400).json({ error: 'Invalid client ID' });
+
+ // Get profile and recent data
+ const profile = await fetchClientProfile(clienteId);
+ const now = new Date();
+ const end = now.toISOString().slice(0, 10);
+ const start30 = new Date(now.getTime() - 30 * 86400000).toISOString().slice(0, 10);
+ const start60 = new Date(now.getTime() - 60 * 86400000).toISOString().slice(0, 10);
+
+ const clientData = await fetchClientData(clienteId, start60, end);
+
+ // Count operations in current vs previous 30-day windows
+ const currOps = (clientData.trend.brlUsd || [])
+ .filter(d => d.dia >= start30)
+ .reduce((s, d) => s + d.qtd, 0) +
+ (clientData.trend.usdBrl || [])
+ .filter(d => d.dia >= start30)
+ .reduce((s, d) => s + d.qtd, 0);
+
+ const prevOps = (clientData.trend.brlUsd || [])
+ .filter(d => d.dia < start30 && d.dia >= start60)
+ .reduce((s, d) => s + d.qtd, 0) +
+ (clientData.trend.usdBrl || [])
+ .filter(d => d.dia < start30 && d.dia >= start60)
+ .reduce((s, d) => s + d.qtd, 0);
+
+ // Determine product count
+ let productCount = 0;
+ if (profile.brlUsd && profile.brlUsd.qtd > 0) productCount++;
+ if (profile.usdBrl && profile.usdBrl.qtd > 0) productCount++;
+
+ const churn = predictChurnRisk({
+ days_inactive: profile.days_inactive,
+ avg_monthly_ops: profile.avg_monthly_ops,
+ avg_monthly_vol: profile.avg_monthly_vol,
+ months_active: profile.months_active,
+ curr_ops: currOps,
+ prev_ops: prevOps,
+ product_count: productCount
+ });
+
+ res.json(churn);
+ } catch (err) {
+ console.error('Churn API error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- Alert API Endpoints ---
+
+app.get('/admin/api/alerts', requireRole('admin'), (req, res) => {
+ try {
+ const unacked = req.query.unacked === '1';
+ const alerts = getAlerts(24, unacked);
+ res.json({ alerts, unacked_count: getUnackedCount() });
+ } catch (err) {
+ console.error('Alerts API error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.put('/admin/api/alerts/:id/ack', requireRole('admin'), (req, res) => {
+ try {
+ const id = parseInt(req.params.id);
+ acknowledgeAlert(id);
+ res.json({ success: true });
+ } catch (err) {
+ console.error('Alert ack error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/admin/api/alerts/history', requireRole('admin'), (req, res) => {
+ try {
+ const days = parseInt(req.query.days) || 7;
+ const alerts = getAlertHistory(days);
+ res.json({ alerts });
+ } catch (err) {
+ console.error('Alert history error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- Health Check ---
+app.get('/health', async (req, res) => {
+ const health = {
+ status: 'ok',
+ uptime: Math.round(process.uptime()),
+ memory: {
+ rss: Math.round(process.memoryUsage().rss / 1024 / 1024),
+ heap: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
+ },
+ cache: cache.stats(),
+ timestamp: new Date().toISOString()
+ };
+ try {
+ const conn = await pool.getConnection();
+ await conn.execute('SELECT 1');
+ conn.release();
+ health.mysql = 'connected';
+ } catch (err) {
+ health.mysql = 'error: ' + err.message;
+ health.status = 'degraded';
+ }
+ res.json(health);
+});
+
+// --- Provider Dashboard (admin only) ---
+app.get('/admin/providers', requireRole('admin'), (req, res) => {
+ try {
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
+ const html = buildAdminProvidersHTML(req.session.user);
+ res.send(html);
+ } catch (err) {
+ console.error('Admin Providers error:', err);
+ res.status(500).send('Erro ao carregar providers: ' + err.message);
+ }
+});
+
+app.get('/admin/api/providers', requireRole('admin'), async (req, res) => {
+ try {
+ const { start, end } = req.query;
+ if (!start || !end) return res.status(400).json({ error: 'start and end required' });
+ const data = await fetchProviderPerformance(start, end);
+ res.json(data);
+ } catch (err) {
+ console.error('Provider API error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/admin/api/providers/failed', requireRole('admin'), async (req, res) => {
+ try {
+ const { start, end } = req.query;
+ if (!start || !end) return res.status(400).json({ error: 'start and end required' });
+ const data = await fetchFailedTransactions(start, end);
+ res.json(data);
+ } catch (err) {
+ console.error('Failed TX API error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/admin/api/providers/trend', requireRole('admin'), async (req, res) => {
+ try {
+ const { start, end } = req.query;
+ if (!start || !end) return res.status(400).json({ error: 'start and end required' });
+ const data = await fetchProviderTrend(start, end);
+ res.json(data);
+ } catch (err) {
+ console.error('Provider Trend API error:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
// Start
app.listen(PORT, () => {
console.log(`BI - CCC rodando: http://localhost:${PORT}`);
@@ -591,4 +960,10 @@ app.listen(PORT, () => {
cache.registerAutoRefresh('top-agentes-30', () => fetchTopAgentes(30), 10 * 60 * 1000);
cache.registerAutoRefresh('top-agentes-7', () => fetchTopAgentes(7), 10 * 60 * 1000);
cache.registerAutoRefresh('top-agentes-90', () => fetchTopAgentes(90), 10 * 60 * 1000);
+
+ // Start alert engine
+ startAlertEngine();
+
+ // Start ETL daily sync (MySQL RDS → SQLite analytics)
+ startETL();
});
diff --git a/src/admin-bi.js b/src/admin-bi.js
index 3d02d89..9127576 100644
--- a/src/admin-bi.js
+++ b/src/admin-bi.js
@@ -410,6 +410,13 @@ function buildAdminBIHTML(user) {
margin-left: auto; font-size: 12px; color: var(--text-muted);
font-weight: 500; background: var(--bg); padding: 6px 12px; border-radius: 6px;
}
+ .export-btn {
+ background: var(--green); color: white; border: none; padding: 8px 16px;
+ border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer;
+ white-space: nowrap; transition: all 0.15s;
+ }
+ .export-btn:hover { opacity: 0.85; transform: translateY(-1px); }
+ [data-theme="dark"] .export-btn { background: rgba(0,255,136,0.15); color: #00FF88; border: 1px solid rgba(0,255,136,0.3); }
/* Hero KPI Cards */
.hero-grid {
@@ -840,6 +847,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
Carregando...
+
@@ -954,6 +962,19 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'bi' })}
+
+
+ 📈
+ Volume Forecast (30 Days)
+
+
+
+
Forecast: Historical + Predicted Volume
+ Loading forecast...
+
+
+
+
⚖
@@ -2093,6 +2114,62 @@ function renderCohort(cohorts, t) {
update();
})();
+var _forecastChart = null;
+async function loadForecast() {
+ try {
+ var resp = await fetch('/admin/api/bi/forecast?metric=volume&days=30');
+ var data = await resp.json();
+ if (!data.historical || data.historical.length === 0) {
+ document.getElementById('forecastInfo').textContent = 'No data';
+ return;
+ }
+ var theme = getChartTheme();
+ var histLabels = data.historical.map(function(d){return d.dia;});
+ var predLabels = data.predicted.map(function(d){return d.dia;});
+ var allLabels = histLabels.concat(predLabels);
+ var histValues = data.historical.map(function(d){return d.vol_usd;});
+ var predValues = new Array(histLabels.length).fill(null).concat(data.predicted.map(function(d){return d.vol_usd;}));
+ var upperValues = new Array(histLabels.length).fill(null).concat(data.confidence_upper.map(function(d){return d.vol_usd;}));
+ var lowerValues = new Array(histLabels.length).fill(null).concat(data.confidence_lower.map(function(d){return d.vol_usd;}));
+ // Extend historical with nulls for prediction period
+ var histFull = histValues.concat(new Array(predLabels.length).fill(null));
+
+ if (_forecastChart) _forecastChart.destroy();
+ var ctx = document.getElementById('forecastChart');
+ if (!ctx) return;
+ _forecastChart = new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: allLabels,
+ datasets: [
+ { label: 'Historical', data: histFull, borderColor: theme.blue, backgroundColor: 'transparent', borderWidth: 2, pointRadius: 0, tension: 0.3 },
+ { label: 'Forecast', data: predValues, borderColor: theme.green, backgroundColor: 'transparent', borderWidth: 2, borderDash: [6,3], pointRadius: 0, tension: 0.3 },
+ { label: 'Upper 95%', data: upperValues, borderColor: 'transparent', backgroundColor: theme.green + '15', fill: '+1', pointRadius: 0 },
+ { label: 'Lower 95%', data: lowerValues, borderColor: 'transparent', backgroundColor: 'transparent', fill: false, pointRadius: 0 }
+ ]
+ },
+ options: {
+ responsive: true, maintainAspectRatio: false,
+ plugins: { legend: { position: 'top', labels: { color: theme.text, usePointStyle: true, pointStyle: 'line', font: { size: 11 } } } },
+ scales: {
+ x: { ticks: { color: theme.text, maxTicksLimit: 15, font: { size: 10 } }, grid: { color: theme.grid } },
+ y: { ticks: { color: theme.text, callback: function(v){return '$' + (v>=1000 ? Math.round(v/1000)+'K' : v);} }, grid: { color: theme.grid } }
+ }
+ }
+ });
+ document.getElementById('forecastInfo').textContent = data.predicted.length + '-day forecast';
+ } catch (e) {
+ console.error('Forecast error:', e);
+ document.getElementById('forecastInfo').textContent = 'Forecast error';
+ }
+}
+
+function exportBIExcel() {
+ var s = document.getElementById('dateStart').value;
+ var e = document.getElementById('dateEnd').value;
+ if (s && e) window.location.href = '/admin/api/export/bi-excel?start=' + s + '&end=' + e;
+}
+
// Init - runs immediately (script is at bottom of body, DOM is ready)
// Also handles case where DOMContentLoaded already fired
function _startBI() {
@@ -2104,7 +2181,7 @@ function _startBI() {
// Apply theme-aware Chart.js defaults
applyChartDefaults(getChartTheme());
- loadBI(); loadRevenue(); loadStrategic(); fetchLiveRate();
+ loadBI(); loadRevenue(); loadStrategic(); loadForecast(); fetchLiveRate();
setInterval(fetchLiveRate, 3000);
// Re-render charts on theme toggle
diff --git a/src/admin-cliente.js b/src/admin-cliente.js
index dea0cf7..610f692 100644
--- a/src/admin-cliente.js
+++ b/src/admin-cliente.js
@@ -205,6 +205,9 @@ function buildAdminClienteHTML(user) {
.date-inputs label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.date-inputs input[type="date"] { padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: inherit; background: var(--bg); color: var(--text); }
.period-info { margin-left: auto; font-size: 12px; color: var(--text-muted); font-weight: 500; background: var(--bg); padding: 6px 12px; border-radius: 6px; }
+ .export-btn { background: var(--green); color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: all 0.15s; }
+ .export-btn:hover { opacity: 0.85; transform: translateY(-1px); }
+ [data-theme="dark"] .export-btn { background: rgba(0,255,136,0.15); color: #00FF88; border: 1px solid rgba(0,255,136,0.3); }
/* === Hero KPIs === */
.hero-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; margin-bottom: 28px; }
@@ -413,6 +416,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
--
Health
+
@@ -431,6 +435,7 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
--
+
@@ -904,6 +909,10 @@ function clearClient() {
function loadProfile() {
fetch('/admin/api/cliente/' + selectedClientId + '/profile').then(function(r){return r.json();}).then(function(data) {
profileData = data; renderProfile(data);
+ // Load churn risk
+ fetch('/admin/api/cliente/' + selectedClientId + '/churn').then(function(r){return r.json();}).then(function(churn) {
+ renderChurnRisk(churn);
+ }).catch(function(){});
});
}
function renderProfile(p) {
@@ -932,6 +941,23 @@ function renderProfile(p) {
});
}
+function renderChurnRisk(churn) {
+ var el = document.getElementById('churnRisk');
+ if (!el) return;
+ var colors = { low: 'var(--green)', medium: 'var(--orange)', high: 'var(--red)', critical: 'var(--red)' };
+ var labels = { low: 'Low Risk', medium: 'Medium Risk', high: 'High Risk', critical: 'Critical' };
+ el.innerHTML = '' +
+ '
' + churn.score + '
' +
+ '
' + labels[churn.risk] + '
' +
+ '
Health: ' + churn.health_score + '/100
' +
+ '' +
+ (churn.factors || []).slice(0,3).map(function(f){
+ var ic = f.status === 'good' ? '✅' : f.status === 'warning' ? '⚠' : '❌';
+ return ic + ' ' + f.name + ': ' + f.score + '/100';
+ }).join(' ') + '
';
+}
+
// === Data Loading ===
function loadData() {
if (!selectedClientId) return;
diff --git a/src/admin-providers.js b/src/admin-providers.js
new file mode 100644
index 0000000..2a04bd9
--- /dev/null
+++ b/src/admin-providers.js
@@ -0,0 +1,1074 @@
+/**
+ * Admin Providers Dashboard - Provider Performance Analysis
+ * Admin-only: provider comparison, success rates, volume analysis, failed transaction breakdown
+ */
+const { buildHeader, buildFooter, buildHead, getChartJsScript } = require('./ui-template');
+
+function buildAdminProvidersHTML(user) {
+ const role = user.role || 'admin';
+ const pageScripts = getChartJsScript();
+
+ const now = new Date();
+ const today = now.toISOString().slice(0, 10);
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 86400000).toISOString().slice(0, 10);
+
+ const pageCSS = `
+ /* Smooth scroll for anchor navigation */
+ html { scroll-behavior: smooth; scroll-padding-top: 20px; }
+
+ /* === TRADING CONSOLE: Light Mode (professional clean) === */
+ body.trading-console {
+ --tc-accent: #1E8E3E;
+ --tc-accent-bg: rgba(30,142,62,0.08);
+ --tc-accent-border: rgba(30,142,62,0.15);
+ --tc-glass: rgba(255,255,255,0.85);
+ --tc-grid: rgba(0,0,0,0.06);
+ background: var(--bg);
+ color: var(--text);
+ }
+
+ /* === TRADING CONSOLE: Dark Mode (Bloomberg terminal) === */
+ [data-theme="dark"] body.trading-console {
+ --bg: #0D1117;
+ --card: #131A24;
+ --text: #E2E8F0;
+ --text-secondary: #94A3B8;
+ --text-muted: #64748B;
+ --border: rgba(0,255,136,0.1);
+ --green: #00FF88;
+ --green-bg: rgba(0,255,136,0.08);
+ --blue: #58A6FF;
+ --blue-bg: rgba(88,166,255,0.08);
+ --orange: #F0883E;
+ --orange-bg: rgba(240,136,62,0.08);
+ --red: #FF4444;
+ --red-bg: rgba(255,68,68,0.08);
+ --purple: #BC8CFF;
+ --purple-bg: rgba(188,140,255,0.08);
+ --admin-accent: #00FF88;
+ --admin-bg: rgba(0,255,136,0.05);
+ --tc-accent: #00FF88;
+ --tc-accent-bg: rgba(0,255,136,0.08);
+ --tc-accent-border: rgba(0,255,136,0.15);
+ --tc-glass: rgba(15,25,35,0.92);
+ --tc-grid: rgba(0,255,136,0.06);
+ background: #0A0F18 !important;
+ color: var(--text);
+ color-scheme: dark;
+ }
+
+ /* Console Cards - Light */
+ body.trading-console .hero-card,
+ body.trading-console .chart-card,
+ body.trading-console .metric-card,
+ body.trading-console .filter-bar {
+ background: var(--card);
+ border: 1px solid var(--border);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+ }
+ /* Console Cards - Dark */
+ [data-theme="dark"] body.trading-console .hero-card,
+ [data-theme="dark"] body.trading-console .chart-card,
+ [data-theme="dark"] body.trading-console .metric-card,
+ [data-theme="dark"] body.trading-console .filter-bar {
+ background: rgba(255,255,255,0.03);
+ border: 1px solid rgba(0,255,136,0.1);
+ box-shadow: 0 2px 12px rgba(0,0,0,0.3), inset 0 1px 0 rgba(0,255,136,0.05);
+ }
+
+ /* Console Values - Light: clean professional */
+ body.trading-console .hero-value {
+ font-variant-numeric: tabular-nums;
+ }
+ /* Console Values - Dark: monospace terminal */
+ [data-theme="dark"] body.trading-console .hero-value {
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
+ text-shadow: 0 0 8px rgba(226,232,240,0.15);
+ }
+
+ /* Console Section Titles - Light */
+ body.trading-console .section-title {
+ color: var(--text-secondary);
+ letter-spacing: 1px;
+ }
+ body.trading-console .section-title .icon {
+ background: var(--tc-accent-bg) !important;
+ color: var(--tc-accent) !important;
+ }
+ /* Console Section Titles - Dark */
+ [data-theme="dark"] body.trading-console .section-title {
+ color: rgba(0,255,136,0.7);
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
+ letter-spacing: 2px;
+ }
+ [data-theme="dark"] body.trading-console .section-title .icon {
+ background: rgba(0,255,136,0.08) !important;
+ color: #00FF88 !important;
+ }
+
+ /* Console Tables - Light */
+ body.trading-console .data-table th {
+ background: var(--bg);
+ color: var(--text-muted);
+ }
+ /* Console Tables - Dark */
+ [data-theme="dark"] body.trading-console .data-table th {
+ background: rgba(0,255,136,0.03);
+ color: rgba(0,255,136,0.6);
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
+ }
+ [data-theme="dark"] body.trading-console .data-table td {
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
+ font-size: 12px;
+ border-bottom-color: rgba(0,255,136,0.06);
+ }
+ [data-theme="dark"] body.trading-console .data-table tr:hover td {
+ background: rgba(0,255,136,0.05);
+ }
+
+ /* Console Buttons - Dark only overrides */
+ [data-theme="dark"] body.trading-console .preset-btn {
+ background: rgba(255,255,255,0.03);
+ color: var(--text-secondary);
+ border-color: rgba(0,255,136,0.1);
+ }
+ [data-theme="dark"] body.trading-console .preset-btn:hover {
+ border-color: #00FF88; color: #00FF88;
+ }
+ [data-theme="dark"] body.trading-console .preset-btn.active {
+ background: rgba(0,255,136,0.15);
+ color: #00FF88; border-color: rgba(0,255,136,0.3);
+ }
+ [data-theme="dark"] body.trading-console .date-inputs input[type="date"] {
+ background: rgba(255,255,255,0.03);
+ color: var(--text); border-color: rgba(0,255,136,0.1);
+ }
+
+ /* Console Loading - Dark */
+ [data-theme="dark"] body.trading-console .loading-overlay { background: rgba(10,15,24,0.85); }
+
+ /* Console Footer - Dark */
+ [data-theme="dark"] body.trading-console .app-footer {
+ background: #0A0F18; border-top-color: rgba(0,255,136,0.1); color: var(--text-muted);
+ }
+
+ /* Console Scrollbars - Dark */
+ [data-theme="dark"] body.trading-console ::-webkit-scrollbar { width: 6px; height: 6px; }
+ [data-theme="dark"] body.trading-console ::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); }
+ [data-theme="dark"] body.trading-console ::-webkit-scrollbar-thumb { background: rgba(0,255,136,0.2); border-radius: 3px; }
+ [data-theme="dark"] body.trading-console ::-webkit-scrollbar-thumb:hover { background: rgba(0,255,136,0.35); }
+
+ /* Filter Bar */
+ .filter-bar {
+ background: var(--card); border-radius: 16px; padding: 20px 24px;
+ border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+ margin-bottom: 24px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
+ }
+ .filter-bar-label { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
+ .filter-presets { display: flex; gap: 8px; }
+ .preset-btn {
+ padding: 8px 16px; border: 1px solid var(--border); border-radius: 8px;
+ background: var(--bg); font-size: 13px; font-weight: 600; cursor: pointer;
+ color: var(--text-secondary); transition: all 0.15s; font-family: inherit;
+ }
+ .preset-btn:hover { border-color: var(--admin-accent); color: var(--admin-accent); }
+ .preset-btn.active {
+ background: var(--admin-accent); color: white; border-color: var(--admin-accent);
+ }
+ .filter-divider { width: 1px; height: 32px; background: var(--border); }
+ .date-inputs { display: flex; align-items: center; gap: 8px; }
+ .date-inputs label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
+ .date-inputs input[type="date"] {
+ padding: 8px 12px; border: 1px solid var(--border); border-radius: 8px;
+ font-size: 13px; font-family: inherit; background: var(--bg); color: var(--text);
+ }
+ .period-info {
+ margin-left: auto; font-size: 12px; color: var(--text-muted);
+ font-weight: 500; background: var(--bg); padding: 6px 12px; border-radius: 6px;
+ }
+
+ /* Export Button */
+ .btn-export {
+ padding: 8px 18px; border: 1px solid var(--border); border-radius: 8px;
+ background: var(--bg); font-size: 13px; font-weight: 600; cursor: pointer;
+ color: var(--text-secondary); transition: all 0.15s; font-family: inherit;
+ display: inline-flex; align-items: center; gap: 6px;
+ }
+ .btn-export:hover { border-color: var(--admin-accent); color: var(--admin-accent); }
+ [data-theme="dark"] body.trading-console .btn-export {
+ background: rgba(255,255,255,0.03);
+ color: var(--text-secondary);
+ border-color: rgba(0,255,136,0.1);
+ }
+ [data-theme="dark"] body.trading-console .btn-export:hover {
+ border-color: #00FF88; color: #00FF88;
+ }
+
+ /* Hero KPI Cards */
+ .hero-grid {
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px;
+ }
+ .hero-card {
+ background: var(--card); border-radius: 16px; padding: 20px 18px;
+ border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+ position: relative; overflow: hidden;
+ min-width: 0;
+ }
+ .hero-card::before {
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
+ }
+ .hero-card.providers-count::before { background: linear-gradient(90deg, var(--purple), #AB47BC); }
+ .hero-card.success-rate::before { background: linear-gradient(90deg, var(--green), #4CAF50); }
+ .hero-card.total-volume::before { background: linear-gradient(90deg, var(--blue), #42A5F5); }
+ .hero-card.settlement::before { background: linear-gradient(90deg, var(--orange), #FFA726); }
+ .hero-label {
+ font-size: 11px; font-weight: 700; color: var(--text-muted);
+ text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;
+ }
+ .hero-value {
+ font-size: clamp(14px, 1.6vw, 26px); font-weight: 800; color: var(--text); margin-bottom: 4px;
+ font-variant-numeric: tabular-nums;
+ word-break: break-word; overflow-wrap: break-word;
+ min-width: 0; line-height: 1.2;
+ }
+ .hero-sub { font-size: 12px; color: var(--text-muted); margin-top: 6px; }
+
+ /* Section Headers */
+ .section-title {
+ font-size: 14px; font-weight: 700; color: var(--text-secondary);
+ text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px;
+ display: flex; align-items: center; gap: 8px;
+ }
+ .section-title .icon {
+ width: 28px; height: 28px; border-radius: 8px; display: flex;
+ align-items: center; justify-content: center; font-size: 14px;
+ }
+
+ /* Charts Grid */
+ .charts-row {
+ display: grid; grid-template-columns: 1fr 2fr; gap: 20px; margin-bottom: 28px;
+ }
+ .charts-row.equal { grid-template-columns: 1fr 1fr; }
+ .charts-row.triple { grid-template-columns: 1fr 1fr 1fr; }
+ .chart-card {
+ background: var(--card); border-radius: 16px; padding: 24px;
+ border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+ }
+ .chart-card h3 {
+ font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 16px;
+ display: flex; align-items: center; gap: 8px;
+ }
+ .chart-card h3 .badge {
+ font-size: 10px; padding: 3px 8px; border-radius: 10px;
+ font-weight: 700; background: var(--bg); color: var(--text-muted);
+ }
+ .chart-wrap { position: relative; height: 280px; }
+ .chart-wrap.short { height: 220px; }
+
+ /* Data Tables */
+ .data-table {
+ width: 100%; border-collapse: collapse; font-size: 13px;
+ }
+ .data-table th {
+ text-align: left; padding: 10px 12px; font-weight: 700; font-size: 11px;
+ text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted);
+ border-bottom: 2px solid var(--border);
+ cursor: pointer; user-select: none; white-space: nowrap;
+ }
+ .data-table th:hover { color: var(--tc-accent); }
+ .data-table th .sort-arrow { font-size: 9px; margin-left: 4px; opacity: 0.5; }
+ .data-table th.sorted .sort-arrow { opacity: 1; }
+ .data-table td {
+ padding: 10px 12px; border-bottom: 1px solid var(--border);
+ color: var(--text); font-variant-numeric: tabular-nums;
+ }
+ .data-table tr:last-child td { border-bottom: none; }
+ .data-table tr:hover td { background: var(--bg); }
+
+ /* Status badges */
+ .status-badge {
+ display: inline-block; font-size: 11px; font-weight: 700; padding: 3px 10px;
+ border-radius: 12px;
+ }
+ .status-badge.good { background: var(--green-bg); color: var(--green); }
+ .status-badge.warning { background: var(--orange-bg); color: var(--orange); }
+ .status-badge.bad { background: var(--red-bg); color: var(--red); }
+
+ /* Loading */
+ .loading-overlay {
+ position: absolute; inset: 0; background: rgba(255,255,255,0.8);
+ display: flex; align-items: center; justify-content: center;
+ border-radius: 16px; z-index: 10; font-size: 13px; color: var(--text-muted);
+ }
+
+ /* Responsive - Small Desktop */
+ @media (max-width: 1200px) {
+ .hero-grid { grid-template-columns: repeat(2, 1fr); }
+ .hero-value { font-size: clamp(15px, 2.4vw, 22px); }
+ .charts-row.triple { grid-template-columns: 1fr 1fr; }
+ .charts-row.triple > :last-child { grid-column: span 2; }
+ }
+
+ /* Responsive - Tablet */
+ @media (max-width: 900px) {
+ .charts-row { grid-template-columns: 1fr; }
+ .charts-row.equal { grid-template-columns: 1fr; }
+ .charts-row.triple { grid-template-columns: 1fr; }
+ .charts-row.triple > :last-child { grid-column: span 1; }
+ .filter-divider { display: none; }
+ .filter-bar { gap: 10px; }
+ }
+
+ /* Responsive - Mobile */
+ @media (max-width: 768px) {
+ /* Filter bar - stack vertically */
+ .filter-bar {
+ padding: 14px 16px; gap: 10px;
+ flex-direction: column; align-items: stretch;
+ }
+ .filter-bar-label { text-align: center; }
+ .filter-presets { flex-wrap: wrap; justify-content: center; }
+ .preset-btn { padding: 10px 14px; min-height: 44px; font-size: 13px; flex: 1; min-width: 70px; text-align: center; }
+ .date-inputs {
+ flex-wrap: wrap; justify-content: center; gap: 6px;
+ }
+ .date-inputs input[type="date"] { flex: 1; min-width: 130px; min-height: 44px; }
+ .period-info { margin-left: 0; width: 100%; text-align: center; }
+
+ /* Hero KPIs */
+ .hero-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
+ .hero-card { padding: 16px 18px; border-radius: 12px; }
+ .hero-value { font-size: 22px; }
+ .hero-label { font-size: 10px; }
+ .hero-sub { font-size: 11px; }
+
+ /* Charts - single column */
+ .charts-row, .charts-row.equal, .charts-row.triple { grid-template-columns: 1fr; }
+ .chart-card { padding: 18px; border-radius: 12px; }
+ .chart-wrap { height: 250px; }
+
+ /* Tables - horizontal scroll */
+ .chart-card:has(.data-table) { padding: 18px 0; }
+ .chart-card:has(.data-table) > h3 { padding: 0 18px; }
+ .data-table { font-size: 12px; }
+ .data-table th { padding: 10px 10px; font-size: 10px; white-space: nowrap; }
+ .data-table td { padding: 10px 10px; white-space: nowrap; }
+ [style*="overflow-x:auto"], [style*="overflow:auto"] {
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: thin;
+ }
+
+ /* Section titles */
+ .section-title { font-size: 13px; margin-bottom: 12px; }
+ }
+
+ /* Responsive - Small Mobile */
+ @media (max-width: 480px) {
+ /* Filter compact */
+ .filter-bar { padding: 12px; }
+ .preset-btn { padding: 8px 10px; font-size: 12px; min-height: 40px; }
+ .date-inputs input[type="date"] { font-size: 12px; padding: 8px 10px; min-height: 40px; }
+
+ /* Hero - single column */
+ .hero-grid { grid-template-columns: 1fr; gap: 10px; }
+ .hero-card { padding: 14px 16px; }
+ .hero-value { font-size: 20px; }
+
+ /* Charts compact */
+ .chart-card { padding: 14px; }
+ .chart-wrap { height: 200px; }
+ .chart-wrap.short { height: 160px; }
+ .chart-card h3 { font-size: 13px; }
+
+ /* Tables even more compact */
+ .data-table { font-size: 11px; }
+ .data-table th { padding: 8px 6px; font-size: 9px; }
+ .data-table td { padding: 8px 6px; }
+
+ /* Section titles */
+ .section-title { font-size: 12px; letter-spacing: 0.5px; }
+ .section-title .icon { width: 24px; height: 24px; font-size: 12px; }
+ }
+
+ /* Dark Mode overrides */
+ [data-theme="dark"] .loading-overlay { background: rgba(13,17,23,0.85); }
+ [data-theme="dark"] .preset-btn { background: var(--card); color: var(--text-secondary); border-color: var(--border); }
+ [data-theme="dark"] .preset-btn:hover { border-color: var(--admin-accent); color: var(--green); }
+ [data-theme="dark"] .date-inputs input[type="date"] { background: var(--card); color: var(--text); border-color: var(--border); }
+ [data-theme="dark"] .data-table tr:hover td { background: rgba(255,255,255,0.03); }
+ `;
+
+ return `
+
+
+${buildHead('Provider Performance', pageCSS, pageScripts)}
+
+
+
+${buildHeader({ role: role, userName: user.nome, activePage: 'providers' })}
+
+
+
+
+
+
Periodo:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Carregando...
+
+
+
+
+
+
Total Providers
+
--
+
provedores ativos
+
+
+
Success Rate
+
--
+
taxa geral de sucesso
+
+
+
Volume Total
+
--
+
USD processado no periodo
+
+
+
Avg Settlement
+
--
+
tempo medio de liquidacao
+
+
+
+
+
+ 🏦
+ Provider Comparison
+
+
+
Performance por Provider --
+
+
+
+
+ | Provider ▲ |
+ Flow ▲ |
+ Tx Total ▲ |
+ Success Rate ▲ |
+ Volume USD ▲ |
+ Avg Ticket ▲ |
+ Spread % ▲ |
+ Settlement (h) ▲ |
+
+
+
+ | Carregando... |
+
+
+
+
+
+
+
+ ⚠
+ Failed Transactions Breakdown
+
+
+
+
By Provider --
+
+
+
+ | Provider | Failed Tx | Volume Lost | Failure Rate |
+
+
+ | Carregando... |
+
+
+
+
+
+
By Status
+
+
+
+ | Status | Count | Volume |
+
+
+ | Carregando... |
+
+
+
+
+
+
+
+
+ 📊
+ Provider Analytics
+
+
+
+
Volume por Provider
+
+
+
+
Success Rate por Provider
+
+
+
+
+
Volume Trend por Provider diario
+
+
+
+
+
+${buildFooter()}
+
+