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 --

+
+ + + + + + + +
ProviderFailed TxVolume LostFailure Rate
Carregando...
+
+
+
+

By Status

+
+ + + + + + + +
StatusCountVolume
Carregando...
+
+
+
+ + +
+ 📊 + Provider Analytics +
+
+
+

Volume por Provider

+
+
+
+

Success Rate por Provider

+
+
+
+
+

Volume Trend por Provider diario

+
+
+ +
+ +${buildFooter()} + +