diff --git a/README.md b/README.md index 049341e..2ac1e23 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ Then commands for dependencies of the proyect ```bash npm install react-icons npm install @neondatabase/serverless + +# Módulo de Historial de Personas (US-H01) +npm install jspdf jspdf-autotable # Exportación a PDF +npm install exceljs # Exportación a Excel (.xlsx) con estilos ``` and for run tests diff --git a/package-lock.json b/package-lock.json index 7e65e03..787796c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,12 @@ "@vercel/postgres": "^0.10.0", "bcryptjs": "^3.0.3", "dotenv": "^17.2.3", + "exceljs": "^4.4.0", + "file-saver": "^2.0.5", "jose": "^6.1.1", "jsonwebtoken": "^9.0.2", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", "next": "^15.5.5", "pg": "^8.16.3", "react": "19.1.0", @@ -22,6 +26,7 @@ "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "sweetalert2": "^11.26.2", + "xlsx": "^0.18.5", "zod": "^4.1.12" }, "devDependencies": { @@ -376,7 +381,6 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1045,6 +1049,47 @@ } } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -2627,6 +2672,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/pg": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", @@ -2638,6 +2689,13 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -2658,6 +2716,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vercel/postgres": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@vercel/postgres/-/postgres-0.10.0.tgz", @@ -2911,6 +2976,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -2946,6 +3020,75 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -2966,6 +3109,48 @@ "node": ">=12" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -2998,6 +3183,55 @@ "require-from-string": "^2.0.2" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -3032,12 +3266,62 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bufferutil": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", @@ -3071,6 +3355,39 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3081,12 +3398,54 @@ "node": ">=18" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3094,6 +3453,59 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -3151,6 +3563,12 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3204,6 +3622,16 @@ "license": "MIT", "peer": true }, + "node_modules/dompurify": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -3216,6 +3644,45 @@ "url": "https://dotenvx.com" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3232,6 +3699,15 @@ "dev": true, "license": "ISC" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", @@ -3328,6 +3804,38 @@ "@types/estree": "^1.0.0" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/exceljs/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3338,6 +3846,36 @@ "node": ">=12.0.0" } }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3360,7 +3898,12 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, + "license": "MIT" + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", "license": "MIT" }, "node_modules/flatted": { @@ -3370,13 +3913,34 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3385,6 +3949,22 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3395,6 +3975,27 @@ "node": ">=6.9.0" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -3415,7 +4016,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/html-encoding-sniffer": { @@ -3431,6 +4031,20 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3459,6 +4073,32 @@ "node": ">= 14" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -3469,6 +4109,29 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3476,6 +4139,12 @@ "dev": true, "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==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -3591,6 +4260,74 @@ "npm": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz", + "integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2 || ^3 || ^4" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -3612,6 +4349,57 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -3873,6 +4661,42 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3885,12 +4709,31 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "license": "MIT" }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", @@ -3909,12 +4752,30 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.2.6", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", @@ -3963,6 +4824,39 @@ "node": ">=4" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -4095,6 +4989,15 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -4112,6 +5015,21 @@ ], "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -4125,6 +5043,15 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4132,6 +5059,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/pg": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.19.0.tgz", @@ -4339,6 +5273,12 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4349,6 +5289,16 @@ "node": ">=6" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -4414,6 +5364,50 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4428,6 +5422,13 @@ "node": ">=8" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4438,6 +5439,29 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -4534,6 +5558,12 @@ "node": ">=10" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -4619,6 +5649,18 @@ "node": ">= 10.x" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4626,6 +5668,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -4633,6 +5685,15 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -4669,6 +5730,16 @@ } } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/sweetalert2": { "version": "11.26.21", "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.21.tgz", @@ -4707,6 +5778,32 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4771,6 +5868,15 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4807,6 +5913,15 @@ "node": ">=20" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tsconfck": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", @@ -4864,6 +5979,54 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -4895,6 +6058,32 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -5128,6 +6317,30 @@ "node": ">=8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -5149,6 +6362,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -5163,7 +6397,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, "license": "MIT" }, "node_modules/xtend": { @@ -5182,6 +6415,41 @@ "dev": true, "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==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 70e06ff..a52a0e8 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,12 @@ "@vercel/postgres": "^0.10.0", "bcryptjs": "^3.0.3", "dotenv": "^17.2.3", + "exceljs": "^4.4.0", + "file-saver": "^2.0.5", "jose": "^6.1.1", "jsonwebtoken": "^9.0.2", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.7", "next": "^15.5.5", "pg": "^8.16.3", "react": "19.1.0", @@ -24,6 +28,7 @@ "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "sweetalert2": "^11.26.2", + "xlsx": "^0.18.5", "zod": "^4.1.12" }, "devDependencies": { diff --git a/scratch-check.ts b/scratch-check.ts new file mode 100644 index 0000000..7712088 --- /dev/null +++ b/scratch-check.ts @@ -0,0 +1,14 @@ +import { db } from './src/lib/db'; + +async function check() { + try { + const res = await db.query('SELECT * FROM reportes_asistencia LIMIT 10'); + console.log('Reportes:', res.rows); + const res2 = await db.query('SELECT * FROM auditoria LIMIT 10'); + console.log('Auditoria:', res2.rows); + } catch (e) { + console.error(e); + } +} + +check(); diff --git a/src/__tests__/permisos/permisos.api.test.ts b/src/__tests__/api/permisos.api.test.ts similarity index 100% rename from src/__tests__/permisos/permisos.api.test.ts rename to src/__tests__/api/permisos.api.test.ts diff --git a/src/__tests__/historial/historial.service.test.ts b/src/__tests__/historial/historial.service.test.ts new file mode 100644 index 0000000..addece2 --- /dev/null +++ b/src/__tests__/historial/historial.service.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { HistorialService } from '@/services/historial.service'; +import { HistorialDAO } from '@/dao/historial.dao'; +import { ConsultaHistorialRequest } from '@/dto/historial.dto'; + +vi.mock('@/dao/historial.dao'); + +describe('HistorialService', () => { + let historialService: HistorialService; + let mockDAO: any; + + beforeEach(() => { + mockDAO = new HistorialDAO(); + + // Inyectamos el mock DAO en el servicio de manera segura + // (en una app real seria mejor usar inyeccion de dependencias via constructor) + historialService = new HistorialService(); + (historialService as any).historialDAO = mockDAO; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + + + it('debe retornar el historial correctamente si los datos son válidos', async () => { + const req: ConsultaHistorialRequest = { + personaId: 1, + tipoPersona: 'asociado' + }; + + const mockResponse = { + persona: { + id: 1, + nombre: 'Isaac', + tipo: 'asociado' + }, + historial: [ + { + id_registro: 1, + tipo: 'asistencia' as any, + fecha: new Date(), + descripcion: 'Asistencia' + } + ] + }; + + mockDAO.obtenerHistorialCompleto.mockResolvedValue(mockResponse); + + const result = await historialService.obtenerHistorial(req); + + expect(mockDAO.obtenerHistorialCompleto).toHaveBeenCalledWith(req); + expect(result).toEqual(mockResponse); + }); +}); diff --git a/src/app/api/debug-db/route.ts b/src/app/api/debug-db/route.ts new file mode 100644 index 0000000..7a64c98 --- /dev/null +++ b/src/app/api/debug-db/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/lib/db'; + +export async function GET() { + try { + const reportes = await db.query('SELECT * FROM reportes_asistencia'); + const auditoria = await db.query('SELECT * FROM auditoria ORDER BY fecha DESC LIMIT 10'); + + return NextResponse.json({ reportes: reportes.rows, auditoria: auditoria.rows }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/eventos/[id]/route.ts b/src/app/api/eventos/[id]/route.ts index 0c09f14..bc1f854 100644 --- a/src/app/api/eventos/[id]/route.ts +++ b/src/app/api/eventos/[id]/route.ts @@ -150,10 +150,19 @@ export async function PUT( valores.push(id); const result = await db.query(sql, valores); + const eventoActualizado = result.rows[0]; + + // Auditoría + try { + const { AuditoriaDAO } = require("@/dao/auditoria.dao"); + const desc = data.activo === false ? `Evento desactivado: ${eventoActualizado.nombre}` : `Evento actualizado: ${eventoActualizado.nombre}`; + await AuditoriaDAO.registrar('eventos', id, data.activo === false ? 'eliminacion' : 'edicion', desc); + } catch (e) { console.error("Error auditando evento", e); } + return NextResponse.json({ success: true, message: "Evento actualizado exitosamente", - data: result.rows[0], + data: eventoActualizado, }); } catch (error: any) { const msg = error?.message || ""; @@ -210,10 +219,18 @@ export async function DELETE( [id] ); + const eventoEliminado = result.rows[0]; + + // Auditoría + try { + const { AuditoriaDAO } = require("@/dao/auditoria.dao"); + await AuditoriaDAO.registrar('eventos', id, 'eliminacion', `Evento eliminado (Inactivo): ${eventoEliminado.nombre}`); + } catch (e) { console.error("Error auditando evento", e); } + return NextResponse.json({ success: true, message: "Evento eliminado exitosamente", - data: result.rows[0], + data: eventoEliminado, }); } catch (error) { console.error("Error al eliminar evento:", error); diff --git a/src/app/api/eventos/route.ts b/src/app/api/eventos/route.ts index d441da6..ece63eb 100644 --- a/src/app/api/eventos/route.ts +++ b/src/app/api/eventos/route.ts @@ -140,6 +140,12 @@ export async function POST(request: NextRequest) { const result = await db.query(insertSQL, values); const evento: EventoResponse = result.rows[0]; + // Auditoría + try { + const { AuditoriaDAO } = require("@/dao/auditoria.dao"); + await AuditoriaDAO.registrar('eventos', evento.id, 'creacion', `Evento creado: ${evento.nombre}`); + } catch (e) { console.error("Error auditando evento", e); } + return NextResponse.json( { success: true, message: "Evento creado exitosamente", data: evento }, { status: 201 } diff --git a/src/app/api/historial/route.ts b/src/app/api/historial/route.ts new file mode 100644 index 0000000..ac8b7e3 --- /dev/null +++ b/src/app/api/historial/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { HistorialService } from '@/services/historial.service'; +import { ConsultaHistorialRequest, TipoRegistroHistorial } from '@/dto/historial.dto'; +import { validateConsultaHistorialInput } from '@/validators/historial.validator'; + +const historialService = new HistorialService(); + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const personaIdParam = searchParams.get('personaId'); + const tipoPersonaParam = searchParams.get('tipoPersona'); + const fechaDesde = searchParams.get('fechaDesde'); + const fechaHasta = searchParams.get('fechaHasta'); + const tipoRegistro = searchParams.get('tipoRegistro') as TipoRegistroHistorial | 'todos' | null; + + const validation = validateConsultaHistorialInput({ + personaId: personaIdParam, + tipoPersona: tipoPersonaParam, + fechaDesde, + fechaHasta, + tipoRegistro + }); + + if (!validation.ok) { + return NextResponse.json( + { error: 'Errores de validación', issues: validation.issues }, + { status: 400 } + ); + } + + const personaId = parseInt(personaIdParam as string, 10); + + const request: ConsultaHistorialRequest = { + personaId, + tipoPersona: tipoPersonaParam as 'usuario' | 'asociado' | 'congregado', + filtros: { + ...(fechaDesde && { fechaDesde }), + ...(fechaHasta && { fechaHasta }), + ...(tipoRegistro && { tipoRegistro }), + } + }; + + const response = await historialService.obtenerHistorial(request); + + return NextResponse.json(response, { status: 200 }); + } catch (error: any) { + console.error('Error GET /api/historial:', error); + if (error.message.includes('Persona no encontrada')) { + return NextResponse.json({ error: error.message }, { status: 404 }); + } + return NextResponse.json( + { error: 'Error interno del servidor al obtener historial' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/historial/sistema/route.ts b/src/app/api/historial/sistema/route.ts new file mode 100644 index 0000000..eb55db8 --- /dev/null +++ b/src/app/api/historial/sistema/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; +import { HistorialDAO } from '@/dao/historial.dao'; + +const historialDAO = new HistorialDAO(); + +export async function GET() { + try { + const hits = await historialDAO.obtenerHitosGlobales(); + return NextResponse.json({ historial: hits }, { status: 200 }); + } catch (error: any) { + console.error('Error GET /api/historial/sistema:', error); + return NextResponse.json({ error: 'Error al obtener hitos del sistema' }, { status: 500 }); + } +} diff --git a/src/app/historial/page.tsx b/src/app/historial/page.tsx new file mode 100644 index 0000000..1cb2796 --- /dev/null +++ b/src/app/historial/page.tsx @@ -0,0 +1,797 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { FaHistory, FaUser, FaFilter, FaFilePdf, FaFileExcel, FaCalendarAlt } from "react-icons/fa"; +import { HistorialResponseDTO, TipoRegistroHistorial } from "@/dto/historial.dto"; +import Sidebar from "@/components/SideBar"; + +type PersonaSimple = { + id: number; + nombre: string; +}; + +const inputClass = + 'shadow-sm border rounded-lg w-full py-2 px-3 text-gray-700 text-sm leading-tight ' + + 'focus:outline-none focus:ring-2 focus:ring-[#003366]/30 focus:border-[#003366] border-gray-300 transition-colors'; + +export default function HistorialPage() { + const [tipoPersona, setTipoPersona] = useState<"usuario" | "asociado" | "congregado" | "todos">("asociado"); + const [personas, setPersonas] = useState([]); + const [personaId, setPersonaId] = useState(0); + const [cargandoPersonas, setCargandoPersonas] = useState(false); + + const [historialData, setHistorialData] = useState(null); + const [cargandoHistorial, setCargandoHistorial] = useState(false); + const [errorMsg, setErrorMsg] = useState(null); + + // Filtros + const [fechaDesde, setFechaDesde] = useState(""); + const [fechaHasta, setFechaHasta] = useState(""); + const [tipoRegistro, setTipoRegistro] = useState("todos"); + + // Paginación + const [paginaActual, setPaginaActual] = useState(1); + const [registrosPorPagina, setRegistrosPorPagina] = useState(20); + + useEffect(() => { + // En modo "todos" absoluto no cargamos lista de personas + if (tipoPersona === "todos") { + setPersonas([]); + setPersonaId(0); + setHistorialData(null); + return; + } + + const fetchPersonas = async () => { + setCargandoPersonas(true); + try { + let endpoint = ""; + if (tipoPersona === "usuario") endpoint = "/api/usuarios"; + else if (tipoPersona === "asociado") endpoint = "/api/asociados"; + else if (tipoPersona === "congregado") endpoint = "/api/congregados"; + + const res = await fetch(endpoint); + const data = await res.json(); + + let list: any[] = []; + if (Array.isArray(data)) { + list = data; + } else if (data && data.data && Array.isArray(data.data)) { + list = data.data; + } + + const listMapped = list.map(p => ({ + id: p.id, + nombre: p.nombreCompleto || p.nombre_completo || p.nombre || p.username || "Sin nombre" + })); + + // Añadimos la opción "Todos los [Tipo]s" + let labelAgregada = ""; + if (tipoPersona === "asociado") labelAgregada = "Todos los Asociados"; + if (tipoPersona === "usuario") labelAgregada = "Todos los Usuarios Staff"; + if (tipoPersona === "congregado") labelAgregada = "Todos los Congregados"; + + setPersonas([ + { id: -1, nombre: labelAgregada }, + ...listMapped + ]); + setPersonaId(0); + setHistorialData(null); + } catch (err) { + console.error("Error al cargar personas", err); + setPersonas([]); + } finally { + setCargandoPersonas(false); + } + }; + fetchPersonas(); + }, [tipoPersona]); + + const fetchHistorial = async () => { + // Si es "todos" absoluto o si se seleccionó "Todos los [Tipo]s" (id -1) + if (tipoPersona === "todos" || personaId === -1) { + await fetchHistorialGeneral(); + return; + } + if (!personaId) return; + setCargandoHistorial(true); + setErrorMsg(null); + setHistorialData(null); + setPaginaActual(1); // reset al nueva búsqueda + + try { + const params = new URLSearchParams({ + personaId: personaId.toString(), + tipoPersona + }); + + if (fechaDesde) params.append("fechaDesde", fechaDesde); + if (fechaHasta) params.append("fechaHasta", fechaHasta); + if (tipoRegistro && tipoRegistro !== "todos") params.append("tipoRegistro", tipoRegistro); + + const res = await fetch(`/api/historial?${params.toString()}`); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.error || "Error al obtener historial"); + } + + setHistorialData(data); + } catch (err: any) { + setErrorMsg(err.message); + } finally { + setCargandoHistorial(false); + } + }; + + // Historial general: une los historiales de todas las personas de todos los tipos + const fetchHistorialGeneral = async () => { + setCargandoHistorial(true); + setErrorMsg(null); + setHistorialData(null); + try { + // Obtenemos solo lo necesario según la selección + const esFiltradoPorTipo = personaId === -1; + + const fetchAsociados = (tipoPersona === "todos" || (esFiltradoPorTipo && tipoPersona === "asociado")) ? fetch("/api/asociados").then(r => r.json()) : Promise.resolve([]); + const fetchUsuarios = (tipoPersona === "todos" || (esFiltradoPorTipo && tipoPersona === "usuario")) ? fetch("/api/usuarios").then(r => r.json()) : Promise.resolve([]); + const fetchCongregados = (tipoPersona === "todos" || (esFiltradoPorTipo && tipoPersona === "congregado")) ? fetch("/api/congregados").then(r => r.json()) : Promise.resolve([]); + const fetchSistema = (tipoPersona === "todos") ? fetch("/api/historial/sistema").then(r => r.json()) : Promise.resolve({ historial: [] }); + + const [resAso, resUsr, resCon, resSys] = await Promise.all([fetchAsociados, fetchUsuarios, fetchCongregados, fetchSistema]); + + const extraerLista = (d: any): any[] => + Array.isArray(d) ? d : Array.isArray(d?.data) ? d.data : []; + + const asociados = extraerLista(resAso).map((p: any) => ({ id: p.id, tipo: 'asociado' })); + const usuarios = extraerLista(resUsr).map((p: any) => ({ id: p.id, tipo: 'usuario' })); + const congregados = extraerLista(resCon).map((p: any) => ({ id: p.id, tipo: 'congregado' })); + + const todas = [...asociados, ...usuarios, ...congregados]; + + // Consultamos el historial de cada persona en paralelo (máx 10 a la vez) + const resultados: HistorialResponseDTO[] = []; + const chunkSize = 10; + for (let i = 0; i < todas.length; i += chunkSize) { + const chunk = todas.slice(i, i + chunkSize); + const parciales = await Promise.allSettled( + chunk.map(p => + fetch(`/api/historial?personaId=${p.id}&tipoPersona=${p.tipo}`).then(r => r.json()) + ) + ); + parciales.forEach(r => { + if (r.status === 'fulfilled' && r.value?.historial) resultados.push(r.value); + }); + } + + // Consolidamos en un único "HistorialResponseDTO" genérico + const todosItems = resultados.flatMap(r => + r.historial.map(item => ({ ...item, _persona: r.persona.nombre })) + ); + + // Añadimos hitos del sistema + if (resSys && resSys.historial) { + todosItems.push(...resSys.historial); + } + + // Deduplicamos por id_registro + const unicos = new Map(); + todosItems.forEach(item => { + if (!unicos.has(item.id_registro)) { + unicos.set(item.id_registro, item); + } + }); + const todosUnicos = Array.from(unicos.values()); + + // Aplicamos filtros de fecha si los hay + const filtrados = todosUnicos.filter(item => { + const t = new Date(item.fecha).getTime(); + if (fechaDesde && t < new Date(fechaDesde).getTime()) return false; + if (fechaHasta && t > new Date(fechaHasta).getTime()) return false; + if (tipoRegistro !== 'todos' && item.tipo !== tipoRegistro) return false; + return true; + }); + + // Ordenar por fecha descendente + filtrados.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime()); + + let labelPersona = "Todos los registros"; + if (personaId === -1) { + if (tipoPersona === "asociado") labelPersona = "Todos los Asociados"; + if (tipoPersona === "usuario") labelPersona = "Todos los Usuarios Staff"; + if (tipoPersona === "congregado") labelPersona = "Todos los Congregados"; + } + + setHistorialData({ + persona: { id: 0, nombre: labelPersona, tipo: tipoPersona }, + historial: filtrados, + } as any); + setPaginaActual(1); // reset al obtener datos nuevos + } catch (err: any) { + setErrorMsg(err.message || "Error al obtener historial general"); + } finally { + setCargandoHistorial(false); + } + }; + + const formatearFecha = (fechaStr: string | Date) => { + const fecha = new Date(fechaStr); + return new Intl.DateTimeFormat('es-CR', { + year: 'numeric', month: 'short', day: 'numeric', + hour: '2-digit', minute: '2-digit' + }).format(fecha); + }; + + // ── Paginación calculada ────────────────────────────────────────────────── + const totalRegistros = historialData?.historial.length ?? 0; + const totalPaginas = Math.max(1, Math.ceil(totalRegistros / registrosPorPagina)); + const inicio = (paginaActual - 1) * registrosPorPagina; + const fin = inicio + registrosPorPagina; + const registrosPagina = historialData?.historial.slice(inicio, fin) ?? []; + + const irAPagina = (n: number) => setPaginaActual(Math.max(1, Math.min(n, totalPaginas))); + + // ── Exportar PDF: plantilla A4 con badge fijo centrado ──────────────────── + + const exportarPDF = async () => { + if (!historialData) return; + + const { jsPDF } = await import("jspdf"); + const autoTable = (await import("jspdf-autotable")).default; + + const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); + + // ── Encabezado ── + doc.setFillColor(0, 51, 102); + doc.rect(0, 0, 210, 28, "F"); + doc.setTextColor(255, 255, 255); + doc.setFontSize(16); doc.setFont("helvetica", "bold"); + doc.text("SCRCR — Iglesia Bíblica Emanuel", 14, 11); + doc.setFontSize(10); doc.setFont("helvetica", "normal"); + doc.text("Historial de Personas", 14, 18); + const hoy = new Intl.DateTimeFormat("es-CR", { + year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" + }).format(new Date()); + doc.setFontSize(8); + doc.text(`Generado: ${hoy}`, 14, 24); + + // ── Sujeto ── + doc.setTextColor(0, 51, 102); doc.setFontSize(13); doc.setFont("helvetica", "bold"); + doc.text(historialData.persona.nombre, 14, 38); + doc.setTextColor(100, 100, 100); doc.setFontSize(9); doc.setFont("helvetica", "normal"); + const tipoLabel = historialData.persona.tipo.charAt(0).toUpperCase() + historialData.persona.tipo.slice(1); + const idLabel = historialData.persona.identificacion ? ` · ${historialData.persona.identificacion}` : ""; + doc.text(`${tipoLabel}${idLabel}`, 14, 44); + doc.setDrawColor(0, 51, 102); doc.setLineWidth(0.5); doc.line(14, 47, 196, 47); + + // ── Tabla ── + // La columna Tipo renderizamos solo el texto blanco; el badge lo dibujamos después + // con willDrawCell para que no se pise con el autoTable text + const BADGE_W = 20; // ancho fijo del badge en mm + const BADGE_H = 6; // alto fijo del badge en mm + const tipoColors: Record = { + asistencia: [22, 163, 74], + permiso: [37, 99, 235], + modificacion: [147, 51, 234], + }; + + autoTable(doc, { + startY: 51, + head: [["Fecha y Hora", "Tipo", "Descripción", "Estado"]], + body: historialData.historial.map(item => [ + formatearFecha(item.fecha), + item.tipo.toUpperCase(), + item.descripcion + (item.detalles?.observaciones ? `\nNota: ${item.detalles.observaciones}` : ""), + item.estado || "—", + ]), + headStyles: { + fillColor: [0, 51, 102], + textColor: 255, + fontStyle: "bold", + fontSize: 9, + cellPadding: 4, + }, + columnStyles: { + 0: { cellWidth: 38 }, + 1: { cellWidth: BADGE_W + 8, halign: "center" }, + 2: { cellWidth: "auto" }, + 3: { cellWidth: 26, halign: "center" }, + }, + alternateRowStyles: { fillColor: [245, 247, 250] }, + bodyStyles: { + fontSize: 8.5, + textColor: [50, 50, 50], + cellPadding: { top: 4, bottom: 4, left: 3, right: 3 }, + minCellHeight: 10, + }, + // 1) Vaciar el texto de la celda "Tipo" para que autoTable no lo renderice + didParseCell: (data: any) => { + if (data.section === "body" && data.column.index === 1) { + data.cell.text = []; + } + }, + // 2) Dibujar el badge una vez terminado el dibujo de la celda + didDrawCell: (data: any) => { + if (data.section === "body" && data.column.index === 1) { + const raw = (data.cell.raw as string).toLowerCase(); + const [r, g, b] = tipoColors[raw] ?? [100, 100, 100]; + + const bx = data.cell.x + (data.cell.width - BADGE_W) / 2; + const by = data.cell.y + (data.cell.height - BADGE_H) / 2; + + data.doc.setFillColor(r, g, b); + data.doc.roundedRect(bx, by, BADGE_W, BADGE_H, 1.5, 1.5, "F"); + + data.doc.setTextColor(255, 255, 255); + data.doc.setFontSize(6.5); + data.doc.setFont("helvetica", "bold"); + data.doc.text( + data.cell.raw as string, + bx + BADGE_W / 2, + by + BADGE_H / 2 + 0.5, + { align: "center", baseline: "middle" } + ); + } + }, + margin: { left: 14, right: 14 }, + }); + + // ── Pie de página ── + const pageCount = (doc as any).internal.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(7); doc.setTextColor(160, 160, 160); + doc.text(`Página ${i} de ${pageCount} · SCRCR — Documento confidencial`, 105, 291, { align: "center" }); + } + + doc.save(`Historial_${historialData.persona.nombre.replace(/\s+/g, "_")}.pdf`); + }; + + // ── Exportar Excel profesional con ExcelJS ──────────────────────────────── + const exportarExcel = async () => { + if (!historialData || historialData.historial.length === 0) return; + + const ExcelJS = (await import("exceljs")).default; + const wb = new ExcelJS.Workbook(); + wb.creator = "SCRCR"; + wb.created = new Date(); + + const ws = wb.addWorksheet("Historial", { + pageSetup: { paperSize: 9, orientation: "landscape" }, + }); + + // ── Ancho de columnas ── + ws.columns = [ + { key: "fecha", width: 24 }, + { key: "tipo", width: 16 }, + { key: "desc", width: 55 }, + { key: "obs", width: 35 }, + { key: "estado", width: 16 }, + ]; + + // ── Fila 1: título grande ── + ws.mergeCells("A1:E1"); + const titleCell = ws.getCell("A1"); + titleCell.value = "SCRCR — Iglesia Bíblica Emanuel"; + titleCell.font = { name: "Calibri", bold: true, size: 16, color: { argb: "FFFFFFFF" } }; + titleCell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF003366" } }; + titleCell.alignment = { vertical: "middle", horizontal: "left", indent: 1 }; + ws.getRow(1).height = 28; + + // ── Fila 2: subtítulo ── + ws.mergeCells("A2:E2"); + const subCell = ws.getCell("A2"); + subCell.value = "Historial de Personas"; + subCell.font = { name: "Calibri", italic: true, size: 11, color: { argb: "FFFFFFFF" } }; + subCell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF003366" } }; + subCell.alignment = { vertical: "middle", horizontal: "left", indent: 1 }; + ws.getRow(2).height = 18; + + // ── Fila 3: espacio ── + ws.getRow(3).height = 6; + + // ── Filas de metadatos (4-7) ── + const metaStyle = { + font: { name: "Calibri", size: 10 }, + alignment: { vertical: "middle" as const }, + }; + const metaLabelStyle = { + font: { name: "Calibri", bold: true, size: 10, color: { argb: "FF003366" } }, + alignment: { vertical: "middle" as const }, + }; + + const metaRows: [string, string][] = [ + ["Nombre:", historialData.persona.nombre], + ["Tipo:", historialData.persona.tipo.charAt(0).toUpperCase() + historialData.persona.tipo.slice(1)], + ...(historialData.persona.identificacion ? [["Identificación:", historialData.persona.identificacion] as [string, string]] : []), + ["Generado:", new Intl.DateTimeFormat("es-CR", { dateStyle: "long", timeStyle: "short" }).format(new Date())], + ]; + metaRows.forEach(([label, val]) => { + const row = ws.addRow([label, val]); + row.height = 16; + row.getCell(1).font = metaLabelStyle.font; + row.getCell(1).alignment = metaLabelStyle.alignment; + row.getCell(2).font = metaStyle.font; + row.getCell(2).alignment = metaStyle.alignment; + }); + + // Espacio antes de la tabla + ws.addRow([]); + + // ── Fila de encabezados de la tabla ── + const headerRow = ws.addRow(["Fecha y Hora", "Tipo", "Descripción", "Observaciones", "Estado"]); + headerRow.height = 20; + headerRow.eachCell(cell => { + cell.font = { name: "Calibri", bold: true, size: 10, color: { argb: "FFFFFFFF" } }; + cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF003366" } }; + cell.alignment = { vertical: "middle", horizontal: "center", wrapText: false }; + cell.border = { + top: { style: "thin", color: { argb: "FFFFFFFF" } }, + bottom: { style: "thin", color: { argb: "FFFFFFFF" } }, + left: { style: "thin", color: { argb: "FFFFFFFF" } }, + right: { style: "thin", color: { argb: "FFFFFFFF" } }, + }; + }); + + // ── Colores de badge por tipo ── + const tipoBg: Record = { + permiso: "FFdbeafe", // azul suave + asistencia: "FFdcfce7", // verde suave + modificacion: "FFf3e8ff", // morado suave + }; + const tipoFg: Record = { + permiso: "FF1e40af", + asistencia: "FF15803d", + modificacion: "FF6b21a8", + }; + + // ── Filas de datos ── + historialData.historial.forEach((item, i) => { + const dataRow = ws.addRow([ + formatearFecha(item.fecha), + item.tipo.toUpperCase(), + item.descripcion, + item.detalles?.observaciones ?? "", + item.estado ?? "", + ]); + dataRow.height = 18; + const isAlt = i % 2 === 1; + + dataRow.eachCell((cell, colNum) => { + cell.font = { name: "Calibri", size: 9.5 }; + cell.alignment = { vertical: "middle", wrapText: colNum === 3 }; + cell.border = { + top: { style: "hair", color: { argb: "FFe2e8f0" } }, + bottom: { style: "hair", color: { argb: "FFe2e8f0" } }, + left: { style: "hair", color: { argb: "FFe2e8f0" } }, + right: { style: "hair", color: { argb: "FFe2e8f0" } }, + }; + // Fondo alternado o blanco + if (colNum === 2) { + // Columna Tipo → color semántico + const tipo = item.tipo.toLowerCase(); + cell.fill = { type: "pattern", pattern: "solid", + fgColor: { argb: tipoBg[tipo] ?? "FFF1F5F9" } }; + cell.font = { name: "Calibri", bold: true, size: 9.5, + color: { argb: tipoFg[tipo] ?? "FF334155" } }; + cell.alignment = { vertical: "middle", horizontal: "center" }; + } else { + cell.fill = { type: "pattern", pattern: "solid", + fgColor: { argb: isAlt ? "FFF8FAFC" : "FFFFFFFF" } }; + } + }); + }); + + // ── Descargar ── + const buffer = await wb.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `Historial_${historialData.persona.nombre.replace(/\s+/g, "_")}.xlsx`; + a.click(); + URL.revokeObjectURL(url); + }; + + const BadgeEstado = ({ tipo }: { tipo: string }) => { + let colorClass = 'bg-gray-100 text-gray-700'; + if (tipo === 'asistencia') colorClass = 'bg-green-100 text-green-700'; + if (tipo === 'permiso') colorClass = 'bg-blue-100 text-blue-700'; + if (tipo === 'modificacion') colorClass = 'bg-purple-100 text-purple-700'; + return ( + + {tipo} + + ); + }; + + return ( +
+ + +
+
+
+ +
+
+

Historial de Personas

+
+
+
+ + {errorMsg && ( +
+ {errorMsg} +
+ )} + +
+ + {/* ── Panel Lateral: Selección y Filtros ── */} +
+ +
+

+ Selección +

+
+
+ + +
+ {/* Ocultar selector de persona solo en modo "todos" absoluto */} + {tipoPersona !== "todos" && ( +
+ + +
+ )} +
+
+ +
+

+ Filtros +

+
+
+ + setFechaDesde(e.target.value)} /> +
+
+ + setFechaHasta(e.target.value)} /> +
+
+ + +
+ +
+
+
+ + {/* ── Panel Principal: Resultados ── */} +
+
+ {!historialData && !cargandoHistorial ? ( +
+ +

+ {(tipoPersona === "todos" || personaId === -1) + ? "Presione \"Consultar Historial\" para ver los registros agrupados." + : "Seleccione una persona y presione consultar."} +

+
+ ) : cargandoHistorial ? ( +
+
+

Cargando...

+
+ ) : ( + <> +
+
+

+ {historialData?.persona.nombre} +

+

+ {historialData?.persona.tipo} + {historialData?.persona.identificacion ? ` · ${historialData.persona.identificacion}` : ""} +

+
+
+ + +
+
+ +
+ {historialData?.historial.length === 0 ? ( +
+ +

No se encontraron registros para los criterios seleccionados.

+
+ ) : ( + + + + + {(tipoPersona === "todos" || personaId === -1) && } + + + + + + + {registrosPagina.map((item: any, idx: number) => ( + + + {(tipoPersona === "todos" || personaId === -1) && ( + + )} + + + + + ))} + +
Fecha y HoraPersonaTipoDescripciónEstado
+ {formatearFecha(item.fecha)} + + {item._persona ?? "—"} + + + + {item.descripcion} + {item.tipo === 'modificacion' && ( + + * Cambios en campos generales. + + )} + {item.detalles?.observaciones && ( + + Nota: {item.detalles.observaciones} + + )} + + {item.estado && ( + {item.estado} + )} +
+ )} +
+ + {/* ── Controles de Paginación ── */} + {totalRegistros > 0 && ( +
+ {/* Selector de registros por página + total */} +
+ Mostrar + + registros por página  ·  {totalRegistros} en total +
+ + {/* Navegación de páginas */} +
+ + + + {/* Números de página — mostramos máx 5 */} + {Array.from({ length: totalPaginas }, (_, i) => i + 1) + .filter(n => n === 1 || n === totalPaginas || Math.abs(n - paginaActual) <= 1) + .reduce<(number | "…")[]>((acc, n, i, arr) => { + if (i > 0 && n - (arr[i - 1] as number) > 1) acc.push("…"); + acc.push(n); + return acc; + }, []) + .map((item, i) => + item === "…" + ? + : + ) + } + + + +
+
+ )} + + )} +
+
+
+ +
+
+
+
+ ); +} diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 0d96974..cbb79f7 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -6,7 +6,7 @@ import { usePathname } from 'next/navigation'; import { FaHome, FaUserPlus, FaList, FaSignOutAlt, FaBars, FaTimes, FaChurch, - FaCalendarAlt, FaUsers, FaChartLine, FaCog, FaClipboardList + FaCalendarAlt, FaUsers, FaChartLine, FaCog, FaClipboardList, FaHistory } from 'react-icons/fa'; import { useAuth } from '@/contexts/AuthContext'; @@ -83,6 +83,13 @@ const NAV_ITEMS: Omit[] = [ label: 'Permisos', roles: ['admin', 'tesorero', 'pastorGeneral'] as Role[], }, + { + id: 'historial', + href: '/historial', + icon: FaHistory, + label: 'Historial', + roles: ['admin', 'tesorero', 'pastorGeneral'] as Role[], + }, { id: 'configuracion', href: '/configuracion', diff --git a/src/dao/asociado.dao.ts b/src/dao/asociado.dao.ts index fa6a065..f41f2c6 100644 --- a/src/dao/asociado.dao.ts +++ b/src/dao/asociado.dao.ts @@ -5,6 +5,7 @@ import { FiltrosAsociadoRequest, } from '@/dto/asociado.dto'; import { Asociado, AsociadoModel } from '@/models/Asociado'; +import { AuditoriaDAO } from './auditoria.dao'; export interface PaginacionResultado { data: T[]; @@ -66,11 +67,9 @@ export class AsociadoDAO { RETURNING * ` as any[]; - if (!result || result.length === 0) { - throw new AsociadoDAOError('No se pudo crear el asociado', 'CREATE_FAILED'); - } - - return this.mapRowToAsociado(result[0]); + const asociado = this.mapRowToAsociado(result[0]); + await AuditoriaDAO.registrar('asociados', asociado.id, 'creacion', 'Registro inicial del asociado'); + return asociado; } catch (error: any) { if (error instanceof AsociadoDAOError) throw error; if (error.code === '23505') { @@ -187,11 +186,9 @@ export class AsociadoDAO { RETURNING * ` as any[]; - if (!result || result.length === 0) { - throw new AsociadoDAOError('Error al actualizar el asociado', 'UPDATE_FAILED'); - } - - return this.mapRowToAsociado(result[0]); + const asociado = this.mapRowToAsociado(result[0]); + await AuditoriaDAO.registrar('asociados', asociado.id, 'edicion', 'Actualización de información del asociado'); + return asociado; } catch (error: any) { if (error instanceof AsociadoDAOError) throw error; if (error.code === '23505') { @@ -206,7 +203,11 @@ export class AsociadoDAO { const result = await this.sql` UPDATE asociados SET estado = 0 WHERE id = ${id} RETURNING id ` as any[]; - return result.length > 0; + if (result.length > 0) { + await AuditoriaDAO.registrar('asociados', id, 'eliminacion', 'Desactivación del asociado (Inactivo)'); + return true; + } + return false; } catch (error) { throw new AsociadoDAOError('Error al eliminar el asociado', 'DATABASE_ERROR', error); } diff --git a/src/dao/auditoria.dao.ts b/src/dao/auditoria.dao.ts new file mode 100644 index 0000000..32a47cf --- /dev/null +++ b/src/dao/auditoria.dao.ts @@ -0,0 +1,15 @@ +import { db } from '@/lib/db'; + +export class AuditoriaDAO { + static async registrar(tabla: string, registroId: number, accion: string, detalles: string) { + try { + await db.query( + 'INSERT INTO auditoria (tabla, registro_id, accion, detalles) VALUES ($1, $2, $3, $4)', + [tabla, registroId, accion, detalles] + ); + } catch (error) { + console.error('Error al registrar auditoría:', error); + // No lanzamos error para no bloquear la operación principal + } + } +} diff --git a/src/dao/congregado.dao.ts b/src/dao/congregado.dao.ts index d5d0f87..81ddc2d 100644 --- a/src/dao/congregado.dao.ts +++ b/src/dao/congregado.dao.ts @@ -5,6 +5,7 @@ import { FiltrosCongregadoRequest, } from '@/dto/congregado.dto'; import { Congregado, CongregadoModel, EstadoCongregado } from '@/models/Congregado'; +import { AuditoriaDAO } from './auditoria.dao'; export interface PaginacionResultado { data: T[]; @@ -68,10 +69,9 @@ export class CongregadoDAO { ] ); - if (!result.rows.length) { - throw new CongregadoDAOError('No se pudo crear el congregado', 'CREATE_FAILED'); - } - return this.mapRowToCongregado(result.rows[0]); + const congregado = this.mapRowToCongregado(result.rows[0]); + await AuditoriaDAO.registrar('congregados', congregado.id, 'creacion', 'Registro inicial del congregado'); + return congregado; } catch (error: any) { if (error instanceof CongregadoDAOError) throw error; if (error.code === '23505') { @@ -214,10 +214,9 @@ export class CongregadoDAO { ] ); - if (!result.rows.length) { - throw new CongregadoDAOError('Error al actualizar el congregado', 'UPDATE_FAILED'); - } - return this.mapRowToCongregado(result.rows[0]); + const congregado = this.mapRowToCongregado(result.rows[0]); + await AuditoriaDAO.registrar('congregados', congregado.id, 'edicion', 'Actualización de información del congregado'); + return congregado; } catch (error: any) { if (error instanceof CongregadoDAOError) throw error; if (error.code === '23505') { @@ -233,7 +232,11 @@ export class CongregadoDAO { 'UPDATE congregados SET estado = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING id', [EstadoCongregado.INACTIVO, id] ); - return result.rows.length > 0; + if (result.rows.length > 0) { + await AuditoriaDAO.registrar('congregados', id, 'eliminacion', 'Desactivación del congregado (Inactivo)'); + return true; + } + return false; } catch (error) { throw new CongregadoDAOError('Error al eliminar el congregado', 'DATABASE_ERROR', error); } diff --git a/src/dao/historial.dao.ts b/src/dao/historial.dao.ts new file mode 100644 index 0000000..1982598 --- /dev/null +++ b/src/dao/historial.dao.ts @@ -0,0 +1,298 @@ +import { db } from '@/lib/db'; +import { + ConsultaHistorialRequest, + HistorialItemDTO, + HistorialResponseDTO, +} from '@/dto/historial.dto'; + +export class HistorialDAOError extends Error { + constructor(message: string, public originalError?: unknown) { + super(message); + this.name = 'HistorialDAOError'; + } +} + +export class HistorialDAO { + + async obtenerPersona(id: number, tipo: string): Promise<{ id: number, nombre: string, identificacion?: string } | null> { + try { + if (tipo === 'usuario') { + const res = await db.query('SELECT id, nombre_completo as nombre, email as identificacion FROM usuarios WHERE id = $1', [id]); + return res.rows.length ? res.rows[0] : null; + } else if (tipo === 'asociado') { + const res = await db.query('SELECT id, nombre_completo as nombre, cedula as identificacion FROM asociados WHERE id = $1', [id]); + return res.rows.length ? res.rows[0] : null; + } else if (tipo === 'congregado') { + const res = await db.query('SELECT id, nombre, cedula as identificacion FROM congregados WHERE id = $1', [id]); + return res.rows.length ? res.rows[0] : null; + } + return null; + } catch (error) { + throw new HistorialDAOError('Error al obtener la persona', error); + } + } + + async obtenerHistorialCompleto(req: ConsultaHistorialRequest): Promise { + const { personaId, tipoPersona, filtros } = req; + + let persona = await this.obtenerPersona(personaId, tipoPersona); + + // Si la persona ya no existe (eliminación permanente), devolvemos un objeto básico + if (!persona) { + persona = { id: personaId, nombre: `Persona Eliminada (ID: ${personaId})`, identificacion: 'N/D' }; + } + + let historial: HistorialItemDTO[] = []; + + // Dependiendo del tipo de persona, buscamos en diferentes tablas. + // Si la persona es un Usuario, mostramos sus permisos + if (tipoPersona === 'usuario' && (!filtros?.tipoRegistro || filtros.tipoRegistro === 'todos' || filtros.tipoRegistro === 'permiso')) { + historial = historial.concat(await this.obtenerPermisosUsuario(personaId)); + } + + // Si la persona es un Asociado, mostramos sus asistencias + if (tipoPersona === 'asociado' && (!filtros?.tipoRegistro || filtros.tipoRegistro === 'todos' || filtros.tipoRegistro === 'asistencia')) { + historial = historial.concat(await this.obtenerAsistenciasAsociado(personaId)); + } + + // Para cualquier tipo de persona, mostramos las "modificaciones" reales de la bitácora + if (!filtros?.tipoRegistro || filtros.tipoRegistro === 'todos' || filtros.tipoRegistro === 'modificacion') { + const modalesAuditoria = await this.obtenerEventosAuditoria(personaId, tipoPersona); + + // Fallback: Si no hay NADA en auditoría (registros viejos), mostramos la simulación básica + if (modalesAuditoria.length === 0) { + historial = historial.concat(await this.obtenerModificaciones(personaId, tipoPersona)); + } else { + historial = historial.concat(modalesAuditoria); + } + } + + // Filtrar por fechas + if (filtros?.fechaDesde) { + const desde = new Date(filtros.fechaDesde).getTime(); + historial = historial.filter(h => new Date(h.fecha).getTime() >= desde); + } + + if (filtros?.fechaHasta) { + const hasta = new Date(filtros.fechaHasta).getTime(); + historial = historial.filter(h => new Date(h.fecha).getTime() <= hasta); + } + + // Ordenar cronológicamente descendente (más reciente primero) + historial.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime()); + + return { + persona: { + id: persona.id, + nombre: persona.nombre, + tipo: tipoPersona, + identificacion: persona.identificacion + }, + historial + }; + } + + private async obtenerPermisosUsuario(usuarioId: number): Promise { + try { + const res = await db.query( + `SELECT id, fecha_inicio, fecha_fin, motivo, estado, created_at, updated_at, observaciones_resolucion + FROM permisos WHERE usuario_id = $1 ORDER BY created_at DESC`, + [usuarioId] + ); + + const items: HistorialItemDTO[] = []; + + res.rows.forEach(row => { + // Solo mostramos resoluciones (Aprobado/Rechazado) en el historial + if (row.estado !== 'PENDIENTE') { + items.push({ + id_registro: 'p-res-' + row.id, + tipo: 'permiso', + fecha: row.updated_at, + descripcion: `Resolución de permiso: ${row.estado}`, + estado: row.estado, + detalles: { + motivo: row.motivo, + observaciones: row.observaciones_resolucion + } + }); + } + }); + + return items; + } catch (error) { + throw new HistorialDAOError('Error al obtener permisos de usuario', error); + } + } + + private async obtenerAsistenciasAsociado(asociadoId: number): Promise { + try { + // Usamos reportes_asistencia que contiene el estado real (presente/ausente/justificado) + const res = await db.query( + `SELECT ra.id, ra.fecha, ra.estado, ra.justificacion, ra.observaciones, + e.nombre as nombre_evento + FROM reportes_asistencia ra + JOIN eventos e ON ra.evento_id = e.id + WHERE ra.asociado_id = $1 ORDER BY ra.fecha DESC`, + [asociadoId] + ); + return res.rows.map(row => { + const estadoLabel = + row.estado === 'presente' ? 'Presente' : + row.estado === 'ausente' ? 'Ausente' : + row.estado === 'justificado' ? 'Justificado' : row.estado; + + return { + id_registro: row.id, + tipo: 'asistencia' as const, + fecha: row.fecha, + descripcion: `${estadoLabel} — ${row.nombre_evento}`, + estado: estadoLabel, + detalles: { + observaciones: row.justificacion || row.observaciones + } + }; + }); + } catch (error) { + throw new HistorialDAOError('Error al obtener asistencias de asociado', error); + } + } + + private async obtenerModificaciones(id: number, tipo: string): Promise { + try { + // Devolvemos el registro de modificación (updated_at y created_at) + const items: HistorialItemDTO[] = []; + let res; + + if (tipo === 'usuario') { + res = await db.query('SELECT created_at, updated_at, estado FROM usuarios WHERE id = $1', [id]); + } else if (tipo === 'asociado') { + res = await db.query('SELECT created_at, updated_at, estado FROM asociados WHERE id = $1', [id]); + } else if (tipo === 'congregado') { + res = await db.query('SELECT created_at, updated_at, estado FROM congregados WHERE id = $1', [id]); + } + + if (res && res.rows.length > 0) { + const { created_at, updated_at, estado } = res.rows[0]; + const statusText = estado === 1 ? 'Activo' : 'Inactivo'; + + const d1 = new Date(created_at).getTime(); + const d2 = new Date(updated_at).getTime(); + + // Debug para ver qué llega de la BD + console.log(`[HistorialDAO] ID:${id} - Created:${d1} - Updated:${d2} - Diff:${d2 - d1}`); + + // Registro de Actualización (si d2 > d1 con margen de 100ms) + const isModified = (d2 - d1) > 100; + + if (isModified) { + items.push({ + id_registro: 'mod-' + id, + tipo: 'modificacion', + fecha: updated_at, + descripcion: `Actualización de perfil (Estado: ${statusText})`, + estado: 'Completado' + }); + } + + // Registro de Creación/Registro Inicial + items.push({ + id_registro: 'creacion-' + id, + tipo: 'modificacion', + fecha: created_at, + descripcion: `Registro inicial de ${tipo} en el sistema`, + estado: 'Completado' + }); + } + + return items; + } catch (error) { + throw new HistorialDAOError('Error al obtener modificaciones de persona', error); + } + } + + private async obtenerEventosAuditoria(id: number, tipo: string): Promise { + try { + const tabla = tipo === 'asociado' ? 'asociados' : tipo === 'congregado' ? 'congregados' : 'usuarios'; + const res = await db.query( + 'SELECT id, accion, detalles, fecha FROM auditoria WHERE tabla = $1 AND registro_id = $2 ORDER BY fecha DESC', + [tabla, id] + ); + + return res.rows.map(row => ({ + id_registro: 'aud-' + row.id, + tipo: 'modificacion', + fecha: row.fecha, + descripcion: row.detalles, + estado: row.accion.toUpperCase(), + })); + } catch (error) { + console.error('Error al obtener eventos de auditoría:', error); + return []; + } + } + + public async obtenerHitosGlobales(): Promise { + try { + const items: HistorialItemDTO[] = []; + + // A. Hitos de la tabla auditoría con nombres reales (Asociados, Congregados, Usuarios y Eventos) + const resAud = await db.query(` + SELECT a.*, + COALESCE(aso.nombre_completo, con.nombre, usu.nombre_completo, e.nombre, 'Sistema') as nombre_persona + FROM auditoria a + LEFT JOIN asociados aso ON a.tabla = 'asociados' AND a.registro_id = aso.id + LEFT JOIN congregados con ON a.tabla = 'congregados' AND a.registro_id = con.id + LEFT JOIN usuarios usu ON a.tabla = 'usuarios' AND a.registro_id = usu.id + LEFT JOIN eventos e ON a.tabla = 'eventos' AND a.registro_id = e.id + ORDER BY a.fecha DESC LIMIT 100 + `); + + resAud.rows.forEach(a => { + // Limpiamos la descripción si viene con el formato viejo "(tabla)" + let desc = a.detalles || ''; + if (desc.includes('(' + a.tabla + ')')) { + desc = desc.split(' (' + a.tabla + ')')[0]; + } + + items.push({ + id_registro: 'aud-' + a.id, + tipo: 'modificacion', + fecha: a.fecha, + descripcion: desc, + estado: a.accion.toUpperCase(), + _persona: a.nombre_persona + } as any); + }); + + // B. Fallback de Eventos (para los que se crearon antes de la tabla auditoría) + const resEventos = await db.query('SELECT id, nombre, created_at FROM eventos ORDER BY created_at DESC LIMIT 50'); + resEventos.rows.forEach(e => { + // Solo añadir si NO hay una auditoría que ya mencione este evento (por ID o por nombre) + const yaExisteEnAuditoria = items.some(i => + (String(i.id_registro).includes('aud-global-') && i.descripcion.includes(e.nombre)) + ); + + if (!yaExisteEnAuditoria) { + items.push({ + id_registro: 'evt-old-' + e.id, + tipo: 'modificacion', + fecha: e.created_at, + descripcion: `Registro de evento: ${e.nombre}`, + estado: 'ACTIVO', + _persona: 'Sistema / Organización' + } as any); + } + }); + + // Ordenamos por fecha descendente + const finalItems = items.sort((a, b) => + new Date(b.fecha).getTime() - new Date(a.fecha).getTime() + ); + + return finalItems; + } catch (error) { + throw new HistorialDAOError('Error al obtener hitos globales', error); + } + } +} diff --git a/src/dto/historial.dto.ts b/src/dto/historial.dto.ts new file mode 100644 index 0000000..851da66 --- /dev/null +++ b/src/dto/historial.dto.ts @@ -0,0 +1,32 @@ +export type TipoRegistroHistorial = 'asistencia' | 'permiso' | 'modificacion'; + +export interface HistorialItemDTO { + id_registro: number | string; + tipo: TipoRegistroHistorial; + fecha: Date | string; + descripcion: string; + estado?: string; + detalles?: any; +} + +export interface HistorialFiltrosRequest { + fechaDesde?: string; + fechaHasta?: string; + tipoRegistro?: TipoRegistroHistorial | 'todos'; +} + +export interface ConsultaHistorialRequest { + personaId: number; + tipoPersona: 'usuario' | 'asociado' | 'congregado'; + filtros?: HistorialFiltrosRequest; +} + +export interface HistorialResponseDTO { + persona: { + id: number; + nombre: string; + tipo: string; + identificacion?: string; + }; + historial: HistorialItemDTO[]; +} diff --git a/src/middleware.ts b/src/middleware.ts index 20e96bd..e6b1e0d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -13,6 +13,7 @@ const routePermissions: Record = { '/dashboard': ['admin', 'tesorero', 'pastorGeneral', 'asistenteAdministrativo'], '/congregados': ['admin', 'tesorero', 'pastorGeneral', 'asistenteAdministrativo'], '/permisos': ['admin', 'tesorero', 'pastorGeneral', 'asistenteAdministrativo'], + '/historial': ['admin', 'tesorero', 'pastorGeneral', 'asistenteAdministrativo'], }; // Rutas que solo pueden acceder usuarios no autenticados @@ -45,7 +46,7 @@ export async function middleware(request: NextRequest) { } // Verificar si la ruta requiere autenticación - const protectedRoute = Object.keys(routePermissions).find(route => + const protectedRoute = Object.keys(routePermissions).find(route => pathname.startsWith(route) ); @@ -61,7 +62,7 @@ export async function middleware(request: NextRequest) { // Verificar el rol del usuario try { console.log('Verificando permisos para:', protectedRoute); - + // Hacer una petición interna para verificar el usuario y su rol const verifyUrl = new URL('/api/auth/verify-role', request.url); const verifyResponse = await fetch(verifyUrl, { @@ -86,11 +87,11 @@ export async function middleware(request: NextRequest) { if (!allowedRoles.includes(rol)) { console.log('Rol no autorizado, redirigiendo a home'); const response = NextResponse.redirect(new URL('/', request.url)); - + // Agregar header para mostrar mensaje de error response.headers.set('X-Unauthorized', 'true'); response.headers.set('X-Unauthorized-Message', 'No tienes permisos para acceder a esta página'); - + return response; } diff --git a/src/services/historial.service.ts b/src/services/historial.service.ts new file mode 100644 index 0000000..0bcb736 --- /dev/null +++ b/src/services/historial.service.ts @@ -0,0 +1,18 @@ +import { HistorialDAO } from '@/dao/historial.dao'; +import { ConsultaHistorialRequest, HistorialResponseDTO } from '@/dto/historial.dto'; + +export class HistorialService { + private historialDAO: HistorialDAO; + + constructor() { + this.historialDAO = new HistorialDAO(); + } + + async obtenerHistorial(req: ConsultaHistorialRequest): Promise { + try { + return await this.historialDAO.obtenerHistorialCompleto(req); + } catch (error: any) { + throw new Error(`Error en HistorialService: ${error.message}`); + } + } +} diff --git a/src/validators/historial.validator.ts b/src/validators/historial.validator.ts new file mode 100644 index 0000000..b9cf64a --- /dev/null +++ b/src/validators/historial.validator.ts @@ -0,0 +1,65 @@ +import { ConsultaHistorialRequest, TipoRegistroHistorial } from '@/dto/historial.dto'; + +export type ValidationIssue = { + field: "personaId" | "tipoPersona" | "fechaDesde" | "fechaHasta" | "tipoRegistro"; + message: string; +}; + +export type ValidationResult = { + ok: boolean; + issues: ValidationIssue[]; +}; + +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; + +export function isValidDateYYYYMMDD(value: string): boolean { + if (!DATE_RE.test(value)) return false; + const [y, m, d] = value.split("-").map(Number); + const dt = new Date(Date.UTC(y, m - 1, d)); + return ( + dt.getUTCFullYear() === y && + dt.getUTCMonth() === m - 1 && + dt.getUTCDate() === d + ); +} + +export function validateConsultaHistorialInput( + input: any +): ValidationResult { + const issues: ValidationIssue[] = []; + + const personaId = Number(input.personaId); + if (!Number.isFinite(personaId) || personaId <= 0) { + issues.push({ field: "personaId", message: "personaId debe ser un número entero positivo." }); + } + + const tiposValidos = ['usuario', 'asociado', 'congregado']; + if (!input.tipoPersona || !tiposValidos.includes(input.tipoPersona)) { + issues.push({ field: "tipoPersona", message: "tipoPersona debe ser usuario, asociado o congregado." }); + } + + if (input.fechaDesde) { + if (!isValidDateYYYYMMDD(input.fechaDesde)) { + issues.push({ field: "fechaDesde", message: "fechaDesde debe tener formato YYYY-MM-DD y ser válida." }); + } + } + + if (input.fechaHasta) { + if (!isValidDateYYYYMMDD(input.fechaHasta)) { + issues.push({ field: "fechaHasta", message: "fechaHasta debe tener formato YYYY-MM-DD y ser válida." }); + } + } + + if (input.fechaDesde && input.fechaHasta && isValidDateYYYYMMDD(input.fechaDesde) && isValidDateYYYYMMDD(input.fechaHasta)) { + if (new Date(input.fechaDesde) > new Date(input.fechaHasta)) { + issues.push({ field: "fechaDesde", message: "fechaDesde no puede ser mayor que fechaHasta." }); + } + } + + const tiposRegistroValidos = ['todos', 'asistencia', 'permiso', 'modificacion']; + if (input.tipoRegistro && !tiposRegistroValidos.includes(input.tipoRegistro)) { + issues.push({ field: "tipoRegistro", message: "tipoRegistro es inválido." }); + } + + return { ok: issues.length === 0, issues }; +}