diff --git a/package-lock.json b/package-lock.json index e803c69..4c73817 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,13 +18,16 @@ "@nestjs/swagger": "^11.2.5", "@nestjs/typeorm": "^11.0.0", "@react-native-community/netinfo": "^11.4.1", + "@types/pdfkit": "^0.17.4", "axios": "^1.13.2", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "exceljs": "^4.4.0", "helmet": "^8.0.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pdfkit": "^0.17.2", "pg": "^8.13.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -845,6 +848,47 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "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/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2691,6 +2735,15 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", "license": "MIT" }, + "node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -2993,6 +3046,15 @@ "@types/passport": "*" } }, + "node_modules/@types/pdfkit": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.4.tgz", + "integrity": "sha512-odAmVuuguRxKh1X4pbMrJMp8ecwNqHRw6lweupvzK+wuyNmi6wzlUlGVZ9EqMvp3Bs2+L9Ty0sRlrvKL+gsQZg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -3727,6 +3789,124 @@ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", "license": "ISC" }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/archiver-utils/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/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -3777,6 +3957,12 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3986,11 +4172,32 @@ "node": ">= 10.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -3998,6 +4205,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -4043,6 +4256,15 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -4103,7 +4325,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -4124,6 +4345,15 @@ "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", @@ -4136,6 +4366,23 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4242,6 +4489,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4581,6 +4840,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -4752,7 +5026,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -4795,6 +5068,31 @@ } } }, + "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/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -4838,6 +5136,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -4986,6 +5290,12 @@ "wrappy": "1" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -5073,6 +5383,51 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/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/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/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5128,6 +5483,15 @@ "node": ">= 0.8" } }, + "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.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -5526,6 +5890,35 @@ "node": ">=0.8.x" } }, + "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/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -5632,11 +6025,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -5872,6 +6277,32 @@ } } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/fontkit/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6026,6 +6457,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -6098,6 +6535,78 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/fstream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6566,6 +7075,12 @@ "node": ">=16.x" } }, + "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/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -7657,6 +8172,13 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7775,6 +8297,54 @@ "npm": ">=6" } }, + "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/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/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", @@ -7816,6 +8386,54 @@ "node": ">=6" } }, + "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/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/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/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7845,6 +8463,15 @@ "integrity": "sha512-v/Ip8k8eYdp7bINpzqDh46V/PaQ8sK+qi97nMQgjZzFlb166YFqlR/HVI+MzsI9JqcyyVWCOipmmretiaSyQyw==", "license": "MIT" }, + "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/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -7870,6 +8497,25 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7877,6 +8523,12 @@ "dev": true, "license": "MIT" }, + "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/load-esm": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", @@ -7932,6 +8584,36 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -7944,12 +8626,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", @@ -7968,6 +8669,12 @@ "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.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7994,6 +8701,18 @@ "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "license": "MIT" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9066,6 +9785,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9230,6 +9955,19 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pdfkit": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", + "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, "node_modules/pg": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz", @@ -9427,6 +10165,11 @@ "node": ">=4" } }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9541,6 +10284,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -9849,6 +10598,27 @@ "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/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -9980,6 +10750,12 @@ "dev": true, "license": "ISC" }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -10126,6 +10902,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -10285,6 +11073,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -10844,6 +11638,22 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -11069,6 +11879,21 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "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/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -11134,6 +11959,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -11650,6 +12484,32 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -11669,6 +12529,60 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/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/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", @@ -12134,6 +13048,12 @@ } } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -12235,6 +13155,84 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } } } } diff --git a/package.json b/package.json index 9b79f69..9a47c67 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,16 @@ "@nestjs/swagger": "^11.2.5", "@nestjs/typeorm": "^11.0.0", "@react-native-community/netinfo": "^11.4.1", + "@types/pdfkit": "^0.17.4", "axios": "^1.13.2", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "exceljs": "^4.4.0", "helmet": "^8.0.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pdfkit": "^0.17.2", "pg": "^8.13.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/src/app.module.ts b/src/app.module.ts index 02a22ff..2d03070 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -75,6 +75,7 @@ import { ExportsModule } from './modules/exports/exports.module'; TemplatesModule, OnboardingModule, SettingsModule, + ExportsModule, ], }) export class AppModule {} diff --git a/src/modules/exports/dto/export-filter.dto.ts b/src/modules/exports/dto/export-filter.dto.ts new file mode 100644 index 0000000..aad6539 --- /dev/null +++ b/src/modules/exports/dto/export-filter.dto.ts @@ -0,0 +1,60 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDateString, IsEnum, IsOptional, IsString } from 'class-validator'; + +export enum ExportFormat { + PDF = 'pdf', + XLSX = 'xlsx', +} + +export class ExportFilterDto { + @ApiPropertyOptional({ description: 'Fecha de inicio (YYYY-MM-DD)' }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiPropertyOptional({ description: 'Fecha de fin (YYYY-MM-DD)' }) + @IsOptional() + @IsDateString() + endDate?: string; +} + +export class SalesExportFilterDto extends ExportFilterDto { + @ApiPropertyOptional({ description: 'Estado de la venta' }) + @IsOptional() + @IsString() + status?: string; +} + +export class InventoryExportFilterDto { + @ApiPropertyOptional({ description: 'ID de categoria' }) + @IsOptional() + @IsString() + categoryId?: string; + + @ApiPropertyOptional({ description: 'Solo productos con stock bajo' }) + @IsOptional() + lowStock?: boolean; +} + +export class FiadosExportFilterDto extends ExportFilterDto { + @ApiPropertyOptional({ description: 'Estado del fiado (pending, partial, paid)' }) + @IsOptional() + @IsString() + status?: string; + + @ApiPropertyOptional({ description: 'Solo fiados vencidos' }) + @IsOptional() + overdue?: boolean; +} + +export class MovementsExportFilterDto extends ExportFilterDto { + @ApiPropertyOptional({ description: 'Tipo de movimiento (purchase, sale, adjustment, loss, return, transfer)' }) + @IsOptional() + @IsString() + movementType?: string; + + @ApiPropertyOptional({ description: 'ID del producto' }) + @IsOptional() + @IsString() + productId?: string; +} diff --git a/src/modules/exports/exports.controller.ts b/src/modules/exports/exports.controller.ts new file mode 100644 index 0000000..42080c3 --- /dev/null +++ b/src/modules/exports/exports.controller.ts @@ -0,0 +1,184 @@ +import { + Controller, + Get, + Param, + Query, + UseGuards, + Request, + Res, + BadRequestException, +} from '@nestjs/common'; +import { Response } from 'express'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +import { ExportsService } from './exports.service'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { + ExportFormat, + SalesExportFilterDto, + InventoryExportFilterDto, + FiadosExportFilterDto, + MovementsExportFilterDto, +} from './dto/export-filter.dto'; + +@ApiTags('exports') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard) +@Controller('v1/exports') +export class ExportsController { + constructor(private readonly exportsService: ExportsService) {} + + @Get('sales/:format') + @ApiOperation({ summary: 'Exportar reporte de ventas' }) + @ApiParam({ name: 'format', enum: ExportFormat, description: 'Formato de exportación (pdf o xlsx)' }) + @ApiQuery({ name: 'startDate', required: false, description: 'Fecha inicio (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: 'Fecha fin (YYYY-MM-DD)' }) + @ApiQuery({ name: 'status', required: false, description: 'Estado de la venta' }) + @ApiResponse({ status: 200, description: 'Archivo de exportación' }) + @ApiResponse({ status: 400, description: 'Formato no válido' }) + async exportSales( + @Request() req: { user: { tenantId: string } }, + @Param('format') format: string, + @Query() filters: SalesExportFilterDto, + @Res() res: Response, + ): Promise { + this.validateFormat(format); + const tenantId = req.user.tenantId; + const filename = `ventas_${this.getDateFilename()}`; + + if (format === ExportFormat.PDF) { + const buffer = await this.exportsService.exportSalesPdf(tenantId, filters); + this.sendPdfResponse(res, buffer, filename); + } else { + const buffer = await this.exportsService.exportSalesExcel(tenantId, filters); + this.sendExcelResponse(res, buffer, filename); + } + } + + @Get('inventory/:format') + @ApiOperation({ summary: 'Exportar reporte de inventario' }) + @ApiParam({ name: 'format', enum: ExportFormat, description: 'Formato de exportación (pdf o xlsx)' }) + @ApiQuery({ name: 'categoryId', required: false, description: 'ID de categoría' }) + @ApiQuery({ name: 'lowStock', required: false, type: Boolean, description: 'Solo productos con stock bajo' }) + @ApiResponse({ status: 200, description: 'Archivo de exportación' }) + @ApiResponse({ status: 400, description: 'Formato no válido' }) + async exportInventory( + @Request() req: { user: { tenantId: string } }, + @Param('format') format: string, + @Query() filters: InventoryExportFilterDto, + @Res() res: Response, + ): Promise { + this.validateFormat(format); + const tenantId = req.user.tenantId; + const filename = `inventario_${this.getDateFilename()}`; + + if (format === ExportFormat.PDF) { + const buffer = await this.exportsService.exportInventoryPdf(tenantId, filters); + this.sendPdfResponse(res, buffer, filename); + } else { + const buffer = await this.exportsService.exportInventoryExcel(tenantId, filters); + this.sendExcelResponse(res, buffer, filename); + } + } + + @Get('fiados/:format') + @ApiOperation({ summary: 'Exportar reporte de fiados (clientes con crédito)' }) + @ApiParam({ name: 'format', enum: ExportFormat, description: 'Formato de exportación (pdf o xlsx)' }) + @ApiQuery({ name: 'startDate', required: false, description: 'Fecha inicio (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: 'Fecha fin (YYYY-MM-DD)' }) + @ApiQuery({ name: 'status', required: false, description: 'Estado del fiado (pending, partial, paid)' }) + @ApiQuery({ name: 'overdue', required: false, type: Boolean, description: 'Solo fiados vencidos' }) + @ApiResponse({ status: 200, description: 'Archivo de exportación' }) + @ApiResponse({ status: 400, description: 'Formato no válido' }) + async exportFiados( + @Request() req: { user: { tenantId: string } }, + @Param('format') format: string, + @Query() filters: FiadosExportFilterDto, + @Res() res: Response, + ): Promise { + this.validateFormat(format); + const tenantId = req.user.tenantId; + const filename = `fiados_${this.getDateFilename()}`; + + if (format === ExportFormat.PDF) { + const buffer = await this.exportsService.exportFiadosPdf(tenantId, filters); + this.sendPdfResponse(res, buffer, filename); + } else { + const buffer = await this.exportsService.exportFiadosExcel(tenantId, filters); + this.sendExcelResponse(res, buffer, filename); + } + } + + @Get('movements/:format') + @ApiOperation({ summary: 'Exportar reporte de movimientos de inventario' }) + @ApiParam({ name: 'format', enum: ExportFormat, description: 'Formato de exportación (pdf o xlsx)' }) + @ApiQuery({ name: 'startDate', required: false, description: 'Fecha inicio (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: 'Fecha fin (YYYY-MM-DD)' }) + @ApiQuery({ name: 'movementType', required: false, description: 'Tipo de movimiento (purchase, sale, adjustment, loss, return, transfer)' }) + @ApiQuery({ name: 'productId', required: false, description: 'ID del producto' }) + @ApiResponse({ status: 200, description: 'Archivo de exportación' }) + @ApiResponse({ status: 400, description: 'Formato no válido' }) + async exportMovements( + @Request() req: { user: { tenantId: string } }, + @Param('format') format: string, + @Query() filters: MovementsExportFilterDto, + @Res() res: Response, + ): Promise { + this.validateFormat(format); + const tenantId = req.user.tenantId; + const filename = `movimientos_${this.getDateFilename()}`; + + if (format === ExportFormat.PDF) { + const buffer = await this.exportsService.exportMovementsPdf(tenantId, filters); + this.sendPdfResponse(res, buffer, filename); + } else { + const buffer = await this.exportsService.exportMovementsExcel(tenantId, filters); + this.sendExcelResponse(res, buffer, filename); + } + } + + // ============ HELPER METHODS ============ + + private validateFormat(format: string): void { + if (format !== ExportFormat.PDF && format !== ExportFormat.XLSX) { + throw new BadRequestException( + `Formato no válido: ${format}. Use 'pdf' o 'xlsx'`, + ); + } + } + + private getDateFilename(): string { + const now = new Date(); + return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`; + } + + private sendPdfResponse(res: Response, buffer: Buffer, filename: string): void { + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}.pdf"`, + 'Content-Length': buffer.length, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }); + res.end(buffer); + } + + private sendExcelResponse(res: Response, buffer: Buffer, filename: string): void { + res.set({ + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': `attachment; filename="${filename}.xlsx"`, + 'Content-Length': buffer.length, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }); + res.end(buffer); + } +} diff --git a/src/modules/exports/exports.module.ts b/src/modules/exports/exports.module.ts new file mode 100644 index 0000000..f499955 --- /dev/null +++ b/src/modules/exports/exports.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExportsController } from './exports.controller'; +import { ExportsService } from './exports.service'; +import { Sale } from '../sales/entities/sale.entity'; +import { Product } from '../products/entities/product.entity'; +import { Fiado } from '../customers/entities/fiado.entity'; +import { InventoryMovement } from '../inventory/entities/inventory-movement.entity'; +import { Customer } from '../customers/entities/customer.entity'; +import { Category } from '../categories/entities/category.entity'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Sale, + Product, + Fiado, + InventoryMovement, + Customer, + Category, + ]), + AuthModule, + ], + controllers: [ExportsController], + providers: [ExportsService], + exports: [ExportsService], +}) +export class ExportsModule {} diff --git a/src/modules/exports/exports.service.ts b/src/modules/exports/exports.service.ts new file mode 100644 index 0000000..17a002a --- /dev/null +++ b/src/modules/exports/exports.service.ts @@ -0,0 +1,848 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, Between, LessThanOrEqual, MoreThanOrEqual, LessThan } from 'typeorm'; +import * as PDFDocument from 'pdfkit'; +import * as ExcelJS from 'exceljs'; +import { Sale, SaleStatus } from '../sales/entities/sale.entity'; +import { Product } from '../products/entities/product.entity'; +import { Fiado, FiadoStatus } from '../customers/entities/fiado.entity'; +import { InventoryMovement } from '../inventory/entities/inventory-movement.entity'; +import { Customer } from '../customers/entities/customer.entity'; +import { Category } from '../categories/entities/category.entity'; +import { + SalesExportFilterDto, + InventoryExportFilterDto, + FiadosExportFilterDto, + MovementsExportFilterDto, +} from './dto/export-filter.dto'; + +@Injectable() +export class ExportsService { + constructor( + @InjectRepository(Sale) + private readonly saleRepository: Repository, + @InjectRepository(Product) + private readonly productRepository: Repository, + @InjectRepository(Fiado) + private readonly fiadoRepository: Repository, + @InjectRepository(InventoryMovement) + private readonly movementRepository: Repository, + @InjectRepository(Customer) + private readonly customerRepository: Repository, + @InjectRepository(Category) + private readonly categoryRepository: Repository, + ) {} + + // ============ SALES EXPORTS ============ + + async getSalesData(tenantId: string, filters: SalesExportFilterDto): Promise { + const query = this.saleRepository + .createQueryBuilder('sale') + .leftJoinAndSelect('sale.items', 'items') + .leftJoinAndSelect('sale.paymentMethod', 'paymentMethod') + .where('sale.tenantId = :tenantId', { tenantId }); + + if (filters.startDate && filters.endDate) { + query.andWhere('DATE(sale.createdAt) BETWEEN :startDate AND :endDate', { + startDate: filters.startDate, + endDate: filters.endDate, + }); + } else if (filters.startDate) { + query.andWhere('DATE(sale.createdAt) >= :startDate', { + startDate: filters.startDate, + }); + } else if (filters.endDate) { + query.andWhere('DATE(sale.createdAt) <= :endDate', { + endDate: filters.endDate, + }); + } + + if (filters.status) { + query.andWhere('sale.status = :status', { status: filters.status }); + } + + query.orderBy('sale.createdAt', 'DESC'); + return query.getMany(); + } + + async exportSalesPdf(tenantId: string, filters: SalesExportFilterDto): Promise { + const sales = await this.getSalesData(tenantId, filters); + const doc = new PDFDocument({ margin: 50, size: 'LETTER' }); + const chunks: Buffer[] = []; + + return new Promise((resolve, reject) => { + doc.on('data', (chunk) => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + + // Header + doc.fontSize(20).text('Reporte de Ventas', { align: 'center' }); + doc.moveDown(0.5); + doc.fontSize(10).text(this.getDateRangeText(filters.startDate, filters.endDate), { align: 'center' }); + doc.moveDown(); + + // Summary + const totalSales = sales.filter(s => s.status === SaleStatus.COMPLETED).length; + const totalRevenue = sales + .filter(s => s.status === SaleStatus.COMPLETED) + .reduce((sum, s) => sum + Number(s.total), 0); + const cancelledCount = sales.filter(s => s.status === SaleStatus.CANCELLED).length; + + doc.fontSize(12).text('Resumen:', { underline: true }); + doc.fontSize(10); + doc.text(`Total de ventas: ${totalSales}`); + doc.text(`Ingresos totales: $${totalRevenue.toFixed(2)}`); + doc.text(`Ventas canceladas: ${cancelledCount}`); + doc.moveDown(); + + // Table header + doc.fontSize(10).text('Detalle de Ventas:', { underline: true }); + doc.moveDown(0.5); + + const tableTop = doc.y; + const col1 = 50; + const col2 = 130; + const col3 = 230; + const col4 = 330; + const col5 = 430; + + doc.font('Helvetica-Bold'); + doc.text('Ticket', col1, tableTop); + doc.text('Fecha', col2, tableTop); + doc.text('Cliente', col3, tableTop); + doc.text('Total', col4, tableTop); + doc.text('Estado', col5, tableTop); + doc.font('Helvetica'); + + let y = tableTop + 20; + doc.moveTo(col1, y - 5).lineTo(530, y - 5).stroke(); + + for (const sale of sales.slice(0, 50)) { // Limit to 50 for PDF + if (y > 700) { + doc.addPage(); + y = 50; + } + + doc.text(sale.ticketNumber || '-', col1, y); + doc.text(new Date(sale.createdAt).toLocaleDateString('es-MX'), col2, y); + doc.text(sale.customerName || 'Público General', col3, y, { width: 90 }); + doc.text(`$${Number(sale.total).toFixed(2)}`, col4, y); + doc.text(this.getStatusLabel(sale.status), col5, y); + y += 20; + } + + if (sales.length > 50) { + doc.moveDown(); + doc.fontSize(8).text(`... y ${sales.length - 50} ventas más. Exporte a Excel para ver todos los registros.`, { align: 'center' }); + } + + // Footer + doc.fontSize(8); + const bottom = doc.page.height - 50; + doc.text(`Generado: ${new Date().toLocaleString('es-MX')}`, 50, bottom, { align: 'center' }); + + doc.end(); + }); + } + + async exportSalesExcel(tenantId: string, filters: SalesExportFilterDto): Promise { + const sales = await this.getSalesData(tenantId, filters); + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'MiChangarrito'; + workbook.created = new Date(); + + const sheet = workbook.addWorksheet('Ventas'); + + // Header styling + sheet.columns = [ + { header: 'Ticket', key: 'ticket', width: 15 }, + { header: 'Fecha', key: 'date', width: 12 }, + { header: 'Hora', key: 'time', width: 10 }, + { header: 'Cliente', key: 'customer', width: 25 }, + { header: 'Subtotal', key: 'subtotal', width: 12 }, + { header: 'Impuesto', key: 'tax', width: 12 }, + { header: 'Descuento', key: 'discount', width: 12 }, + { header: 'Total', key: 'total', width: 12 }, + { header: 'Método Pago', key: 'paymentMethod', width: 15 }, + { header: 'Estado', key: 'status', width: 12 }, + { header: 'Productos', key: 'items', width: 40 }, + ]; + + // Style header row + sheet.getRow(1).font = { bold: true }; + sheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF4472C4' }, + }; + sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } }; + + // Add data + for (const sale of sales) { + const itemsList = sale.items + ?.map(item => `${item.productName} x${item.quantity}`) + .join(', ') || ''; + + sheet.addRow({ + ticket: sale.ticketNumber || '-', + date: new Date(sale.createdAt).toLocaleDateString('es-MX'), + time: new Date(sale.createdAt).toLocaleTimeString('es-MX'), + customer: sale.customerName || 'Público General', + subtotal: Number(sale.subtotal), + tax: Number(sale.taxAmount), + discount: Number(sale.discountAmount), + total: Number(sale.total), + paymentMethod: sale.paymentMethod?.name || 'Efectivo', + status: this.getStatusLabel(sale.status), + items: itemsList, + }); + } + + // Format currency columns + ['E', 'F', 'G', 'H'].forEach(col => { + sheet.getColumn(col).numFmt = '"$"#,##0.00'; + }); + + // Summary section + sheet.addRow([]); + const summaryRow = sheet.addRow(['Resumen']); + summaryRow.font = { bold: true }; + + const completedSales = sales.filter(s => s.status === SaleStatus.COMPLETED); + sheet.addRow(['Total Ventas Completadas', completedSales.length]); + sheet.addRow(['Ingresos Totales', completedSales.reduce((sum, s) => sum + Number(s.total), 0)]); + sheet.addRow(['Ventas Canceladas', sales.filter(s => s.status === SaleStatus.CANCELLED).length]); + + const arrayBuffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(arrayBuffer); + } + + // ============ INVENTORY EXPORTS ============ + + async getInventoryData(tenantId: string, filters: InventoryExportFilterDto): Promise { + const query = this.productRepository + .createQueryBuilder('product') + .leftJoinAndSelect('product.category', 'category') + .where('product.tenantId = :tenantId', { tenantId }) + .andWhere('product.status = :status', { status: 'active' }); + + if (filters.categoryId) { + query.andWhere('product.categoryId = :categoryId', { categoryId: filters.categoryId }); + } + + if (filters.lowStock) { + query.andWhere('product.trackInventory = true') + .andWhere('product.stockQuantity <= product.lowStockThreshold'); + } + + query.orderBy('product.name', 'ASC'); + return query.getMany(); + } + + async exportInventoryPdf(tenantId: string, filters: InventoryExportFilterDto): Promise { + const products = await this.getInventoryData(tenantId, filters); + const doc = new PDFDocument({ margin: 50, size: 'LETTER' }); + const chunks: Buffer[] = []; + + return new Promise((resolve, reject) => { + doc.on('data', (chunk) => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + + // Header + doc.fontSize(20).text('Reporte de Inventario', { align: 'center' }); + doc.moveDown(0.5); + doc.fontSize(10).text(`Generado: ${new Date().toLocaleString('es-MX')}`, { align: 'center' }); + if (filters.lowStock) { + doc.text('(Solo productos con stock bajo)', { align: 'center' }); + } + doc.moveDown(); + + // Summary + const totalProducts = products.length; + const totalValue = products.reduce((sum, p) => sum + (Number(p.price) * Number(p.stockQuantity)), 0); + const lowStockCount = products.filter(p => + p.trackInventory && Number(p.stockQuantity) <= Number(p.lowStockThreshold) + ).length; + + doc.fontSize(12).text('Resumen:', { underline: true }); + doc.fontSize(10); + doc.text(`Total de productos: ${totalProducts}`); + doc.text(`Valor total del inventario: $${totalValue.toFixed(2)}`); + doc.text(`Productos con stock bajo: ${lowStockCount}`); + doc.moveDown(); + + // Table + doc.fontSize(10).text('Listado de Productos:', { underline: true }); + doc.moveDown(0.5); + + const tableTop = doc.y; + const col1 = 50; + const col2 = 200; + const col3 = 280; + const col4 = 340; + const col5 = 410; + const col6 = 480; + + doc.font('Helvetica-Bold'); + doc.text('Producto', col1, tableTop); + doc.text('Categoría', col2, tableTop); + doc.text('SKU', col3, tableTop); + doc.text('Stock', col4, tableTop); + doc.text('Precio', col5, tableTop); + doc.text('Valor', col6, tableTop); + doc.font('Helvetica'); + + let y = tableTop + 20; + doc.moveTo(col1, y - 5).lineTo(550, y - 5).stroke(); + + for (const product of products.slice(0, 50)) { + if (y > 700) { + doc.addPage(); + y = 50; + } + + const stockValue = Number(product.price) * Number(product.stockQuantity); + const isLowStock = product.trackInventory && Number(product.stockQuantity) <= Number(product.lowStockThreshold); + + doc.text(product.name.substring(0, 25), col1, y, { width: 145 }); + doc.text(product.category?.name || '-', col2, y, { width: 75 }); + doc.text(product.sku || '-', col3, y, { width: 55 }); + doc.text(`${product.stockQuantity}${isLowStock ? ' ⚠' : ''}`, col4, y); + doc.text(`$${Number(product.price).toFixed(2)}`, col5, y); + doc.text(`$${stockValue.toFixed(2)}`, col6, y); + y += 20; + } + + if (products.length > 50) { + doc.moveDown(); + doc.fontSize(8).text(`... y ${products.length - 50} productos más. Exporte a Excel para ver todos.`, { align: 'center' }); + } + + doc.end(); + }); + } + + async exportInventoryExcel(tenantId: string, filters: InventoryExportFilterDto): Promise { + const products = await this.getInventoryData(tenantId, filters); + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'MiChangarrito'; + workbook.created = new Date(); + + const sheet = workbook.addWorksheet('Inventario'); + + sheet.columns = [ + { header: 'SKU', key: 'sku', width: 15 }, + { header: 'Código Barras', key: 'barcode', width: 18 }, + { header: 'Producto', key: 'name', width: 30 }, + { header: 'Categoría', key: 'category', width: 20 }, + { header: 'Precio Venta', key: 'price', width: 14 }, + { header: 'Costo', key: 'costPrice', width: 12 }, + { header: 'Stock', key: 'stock', width: 10 }, + { header: 'Mínimo', key: 'threshold', width: 10 }, + { header: 'Unidad', key: 'unit', width: 10 }, + { header: 'Valor Inventario', key: 'value', width: 16 }, + { header: 'Stock Bajo', key: 'lowStock', width: 12 }, + ]; + + sheet.getRow(1).font = { bold: true }; + sheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF70AD47' }, + }; + sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } }; + + for (const product of products) { + const isLowStock = product.trackInventory && + Number(product.stockQuantity) <= Number(product.lowStockThreshold); + const stockValue = Number(product.price) * Number(product.stockQuantity); + + const row = sheet.addRow({ + sku: product.sku || '-', + barcode: product.barcode || '-', + name: product.name, + category: product.category?.name || '-', + price: Number(product.price), + costPrice: product.costPrice ? Number(product.costPrice) : null, + stock: Number(product.stockQuantity), + threshold: product.trackInventory ? Number(product.lowStockThreshold) : '-', + unit: product.unit, + value: stockValue, + lowStock: isLowStock ? 'Sí' : 'No', + }); + + if (isLowStock) { + row.getCell('lowStock').fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFC000' }, + }; + } + } + + ['E', 'F', 'J'].forEach(col => { + sheet.getColumn(col).numFmt = '"$"#,##0.00'; + }); + + // Summary + sheet.addRow([]); + const summaryRow = sheet.addRow(['Resumen']); + summaryRow.font = { bold: true }; + sheet.addRow(['Total Productos', products.length]); + sheet.addRow(['Valor Total Inventario', products.reduce((sum, p) => + sum + (Number(p.price) * Number(p.stockQuantity)), 0)]); + sheet.addRow(['Productos Stock Bajo', products.filter(p => + p.trackInventory && Number(p.stockQuantity) <= Number(p.lowStockThreshold)).length]); + + const arrayBuffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(arrayBuffer); + } + + // ============ FIADOS EXPORTS ============ + + async getFiadosData(tenantId: string, filters: FiadosExportFilterDto): Promise { + const query = this.fiadoRepository + .createQueryBuilder('fiado') + .leftJoinAndSelect('fiado.customer', 'customer') + .leftJoinAndSelect('fiado.payments', 'payments') + .where('fiado.tenantId = :tenantId', { tenantId }); + + if (filters.startDate && filters.endDate) { + query.andWhere('DATE(fiado.createdAt) BETWEEN :startDate AND :endDate', { + startDate: filters.startDate, + endDate: filters.endDate, + }); + } else if (filters.startDate) { + query.andWhere('DATE(fiado.createdAt) >= :startDate', { startDate: filters.startDate }); + } else if (filters.endDate) { + query.andWhere('DATE(fiado.createdAt) <= :endDate', { endDate: filters.endDate }); + } + + if (filters.status) { + query.andWhere('fiado.status = :status', { status: filters.status }); + } + + if (filters.overdue) { + query.andWhere('fiado.dueDate < :today', { today: new Date() }) + .andWhere('fiado.status IN (:...activeStatuses)', { + activeStatuses: [FiadoStatus.PENDING, FiadoStatus.PARTIAL] + }); + } + + query.orderBy('fiado.createdAt', 'DESC'); + return query.getMany(); + } + + async exportFiadosPdf(tenantId: string, filters: FiadosExportFilterDto): Promise { + const fiados = await this.getFiadosData(tenantId, filters); + const doc = new PDFDocument({ margin: 50, size: 'LETTER' }); + const chunks: Buffer[] = []; + + return new Promise((resolve, reject) => { + doc.on('data', (chunk) => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + + // Header + doc.fontSize(20).text('Reporte de Fiados', { align: 'center' }); + doc.moveDown(0.5); + doc.fontSize(10).text(this.getDateRangeText(filters.startDate, filters.endDate), { align: 'center' }); + if (filters.overdue) { + doc.text('(Solo fiados vencidos)', { align: 'center' }); + } + doc.moveDown(); + + // Summary + const activeFiados = fiados.filter(f => + f.status === FiadoStatus.PENDING || f.status === FiadoStatus.PARTIAL + ); + const totalDebt = activeFiados.reduce((sum, f) => sum + Number(f.remainingAmount), 0); + const overdueCount = fiados.filter(f => + f.dueDate && new Date(f.dueDate) < new Date() && + (f.status === FiadoStatus.PENDING || f.status === FiadoStatus.PARTIAL) + ).length; + + doc.fontSize(12).text('Resumen:', { underline: true }); + doc.fontSize(10); + doc.text(`Total de fiados activos: ${activeFiados.length}`); + doc.text(`Deuda total pendiente: $${totalDebt.toFixed(2)}`); + doc.text(`Fiados vencidos: ${overdueCount}`); + doc.moveDown(); + + // Table + doc.fontSize(10).text('Detalle de Fiados:', { underline: true }); + doc.moveDown(0.5); + + const tableTop = doc.y; + const col1 = 50; + const col2 = 150; + const col3 = 240; + const col4 = 310; + const col5 = 380; + const col6 = 450; + + doc.font('Helvetica-Bold'); + doc.text('Cliente', col1, tableTop); + doc.text('Teléfono', col2, tableTop); + doc.text('Monto', col3, tableTop); + doc.text('Pagado', col4, tableTop); + doc.text('Pendiente', col5, tableTop); + doc.text('Vence', col6, tableTop); + doc.font('Helvetica'); + + let y = tableTop + 20; + doc.moveTo(col1, y - 5).lineTo(550, y - 5).stroke(); + + for (const fiado of fiados.slice(0, 50)) { + if (y > 700) { + doc.addPage(); + y = 50; + } + + const isOverdue = fiado.dueDate && new Date(fiado.dueDate) < new Date() && + (fiado.status === FiadoStatus.PENDING || fiado.status === FiadoStatus.PARTIAL); + + doc.text(fiado.customer?.name?.substring(0, 18) || '-', col1, y); + doc.text(fiado.customer?.phone || '-', col2, y); + doc.text(`$${Number(fiado.amount).toFixed(2)}`, col3, y); + doc.text(`$${Number(fiado.paidAmount).toFixed(2)}`, col4, y); + doc.text(`$${Number(fiado.remainingAmount).toFixed(2)}`, col5, y); + doc.text(fiado.dueDate ? `${new Date(fiado.dueDate).toLocaleDateString('es-MX')}${isOverdue ? ' ⚠' : ''}` : '-', col6, y); + y += 20; + } + + if (fiados.length > 50) { + doc.moveDown(); + doc.fontSize(8).text(`... y ${fiados.length - 50} fiados más. Exporte a Excel para ver todos.`, { align: 'center' }); + } + + doc.end(); + }); + } + + async exportFiadosExcel(tenantId: string, filters: FiadosExportFilterDto): Promise { + const fiados = await this.getFiadosData(tenantId, filters); + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'MiChangarrito'; + workbook.created = new Date(); + + const sheet = workbook.addWorksheet('Fiados'); + + sheet.columns = [ + { header: 'Cliente', key: 'customer', width: 25 }, + { header: 'Teléfono', key: 'phone', width: 15 }, + { header: 'Fecha', key: 'date', width: 12 }, + { header: 'Descripción', key: 'description', width: 30 }, + { header: 'Monto Original', key: 'amount', width: 15 }, + { header: 'Pagado', key: 'paid', width: 12 }, + { header: 'Pendiente', key: 'remaining', width: 12 }, + { header: 'Fecha Vencimiento', key: 'dueDate', width: 18 }, + { header: 'Estado', key: 'status', width: 12 }, + { header: 'Vencido', key: 'overdue', width: 10 }, + ]; + + sheet.getRow(1).font = { bold: true }; + sheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFED7D31' }, + }; + sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } }; + + for (const fiado of fiados) { + const isOverdue = fiado.dueDate && new Date(fiado.dueDate) < new Date() && + (fiado.status === FiadoStatus.PENDING || fiado.status === FiadoStatus.PARTIAL); + + const row = sheet.addRow({ + customer: fiado.customer?.name || '-', + phone: fiado.customer?.phone || '-', + date: new Date(fiado.createdAt).toLocaleDateString('es-MX'), + description: fiado.description || '-', + amount: Number(fiado.amount), + paid: Number(fiado.paidAmount), + remaining: Number(fiado.remainingAmount), + dueDate: fiado.dueDate ? new Date(fiado.dueDate).toLocaleDateString('es-MX') : '-', + status: this.getFiadoStatusLabel(fiado.status), + overdue: isOverdue ? 'Sí' : 'No', + }); + + if (isOverdue) { + row.getCell('overdue').fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFF0000' }, + }; + row.getCell('overdue').font = { color: { argb: 'FFFFFFFF' } }; + } + } + + ['E', 'F', 'G'].forEach(col => { + sheet.getColumn(col).numFmt = '"$"#,##0.00'; + }); + + // Summary + sheet.addRow([]); + const summaryRow = sheet.addRow(['Resumen']); + summaryRow.font = { bold: true }; + + const activeFiados = fiados.filter(f => + f.status === FiadoStatus.PENDING || f.status === FiadoStatus.PARTIAL + ); + sheet.addRow(['Fiados Activos', activeFiados.length]); + sheet.addRow(['Deuda Total Pendiente', activeFiados.reduce((sum, f) => sum + Number(f.remainingAmount), 0)]); + sheet.addRow(['Fiados Vencidos', fiados.filter(f => + f.dueDate && new Date(f.dueDate) < new Date() && + (f.status === FiadoStatus.PENDING || f.status === FiadoStatus.PARTIAL) + ).length]); + + const arrayBuffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(arrayBuffer); + } + + // ============ MOVEMENTS EXPORTS ============ + + async getMovementsData(tenantId: string, filters: MovementsExportFilterDto): Promise { + const query = this.movementRepository + .createQueryBuilder('movement') + .leftJoinAndSelect('movement.product', 'product') + .where('movement.tenantId = :tenantId', { tenantId }); + + if (filters.startDate && filters.endDate) { + query.andWhere('DATE(movement.createdAt) BETWEEN :startDate AND :endDate', { + startDate: filters.startDate, + endDate: filters.endDate, + }); + } else if (filters.startDate) { + query.andWhere('DATE(movement.createdAt) >= :startDate', { startDate: filters.startDate }); + } else if (filters.endDate) { + query.andWhere('DATE(movement.createdAt) <= :endDate', { endDate: filters.endDate }); + } + + if (filters.movementType) { + query.andWhere('movement.movementType = :movementType', { movementType: filters.movementType }); + } + + if (filters.productId) { + query.andWhere('movement.productId = :productId', { productId: filters.productId }); + } + + query.orderBy('movement.createdAt', 'DESC'); + return query.getMany(); + } + + async exportMovementsPdf(tenantId: string, filters: MovementsExportFilterDto): Promise { + const movements = await this.getMovementsData(tenantId, filters); + const doc = new PDFDocument({ margin: 50, size: 'LETTER', layout: 'landscape' }); + const chunks: Buffer[] = []; + + return new Promise((resolve, reject) => { + doc.on('data', (chunk) => chunks.push(chunk)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + + // Header + doc.fontSize(20).text('Reporte de Movimientos de Inventario', { align: 'center' }); + doc.moveDown(0.5); + doc.fontSize(10).text(this.getDateRangeText(filters.startDate, filters.endDate), { align: 'center' }); + doc.moveDown(); + + // Summary by type + const typeGroups = this.groupByMovementType(movements); + doc.fontSize(12).text('Resumen por Tipo:', { underline: true }); + doc.fontSize(10); + Object.entries(typeGroups).forEach(([type, items]) => { + doc.text(`${this.getMovementTypeLabel(type)}: ${items.length} movimientos`); + }); + doc.moveDown(); + + // Table + doc.fontSize(10).text('Detalle de Movimientos:', { underline: true }); + doc.moveDown(0.5); + + const tableTop = doc.y; + const col1 = 50; + const col2 = 130; + const col3 = 270; + const col4 = 380; + const col5 = 450; + const col6 = 520; + const col7 = 590; + const col8 = 660; + + doc.font('Helvetica-Bold'); + doc.text('Fecha', col1, tableTop); + doc.text('Producto', col2, tableTop); + doc.text('Tipo', col3, tableTop); + doc.text('Cantidad', col4, tableTop); + doc.text('Antes', col5, tableTop); + doc.text('Después', col6, tableTop); + doc.text('Costo Unit.', col7, tableTop); + doc.text('Notas', col8, tableTop); + doc.font('Helvetica'); + + let y = tableTop + 20; + doc.moveTo(col1, y - 5).lineTo(750, y - 5).stroke(); + + for (const movement of movements.slice(0, 40)) { + if (y > 500) { + doc.addPage(); + y = 50; + } + + const qtySign = Number(movement.quantity) >= 0 ? '+' : ''; + + doc.text(new Date(movement.createdAt).toLocaleDateString('es-MX'), col1, y); + doc.text(movement.product?.name?.substring(0, 22) || '-', col2, y, { width: 135 }); + doc.text(this.getMovementTypeLabel(movement.movementType), col3, y); + doc.text(`${qtySign}${Number(movement.quantity)}`, col4, y); + doc.text(String(Number(movement.quantityBefore)), col5, y); + doc.text(String(Number(movement.quantityAfter)), col6, y); + doc.text(movement.unitCost ? `$${Number(movement.unitCost).toFixed(2)}` : '-', col7, y); + doc.text(movement.notes?.substring(0, 15) || '-', col8, y, { width: 90 }); + y += 20; + } + + if (movements.length > 40) { + doc.moveDown(); + doc.fontSize(8).text(`... y ${movements.length - 40} movimientos más. Exporte a Excel para ver todos.`, { align: 'center' }); + } + + doc.end(); + }); + } + + async exportMovementsExcel(tenantId: string, filters: MovementsExportFilterDto): Promise { + const movements = await this.getMovementsData(tenantId, filters); + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'MiChangarrito'; + workbook.created = new Date(); + + const sheet = workbook.addWorksheet('Movimientos'); + + sheet.columns = [ + { header: 'Fecha', key: 'date', width: 12 }, + { header: 'Hora', key: 'time', width: 10 }, + { header: 'Producto', key: 'product', width: 30 }, + { header: 'SKU', key: 'sku', width: 15 }, + { header: 'Tipo Movimiento', key: 'type', width: 15 }, + { header: 'Cantidad', key: 'quantity', width: 12 }, + { header: 'Stock Antes', key: 'before', width: 12 }, + { header: 'Stock Después', key: 'after', width: 14 }, + { header: 'Costo Unitario', key: 'cost', width: 14 }, + { header: 'Referencia', key: 'reference', width: 15 }, + { header: 'Notas', key: 'notes', width: 30 }, + ]; + + sheet.getRow(1).font = { bold: true }; + sheet.getRow(1).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF7030A0' }, + }; + sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } }; + + for (const movement of movements) { + const row = sheet.addRow({ + date: new Date(movement.createdAt).toLocaleDateString('es-MX'), + time: new Date(movement.createdAt).toLocaleTimeString('es-MX'), + product: movement.product?.name || '-', + sku: movement.product?.sku || '-', + type: this.getMovementTypeLabel(movement.movementType), + quantity: Number(movement.quantity), + before: Number(movement.quantityBefore), + after: Number(movement.quantityAfter), + cost: movement.unitCost ? Number(movement.unitCost) : null, + reference: movement.referenceType ? `${movement.referenceType}: ${movement.referenceId}` : '-', + notes: movement.notes || '-', + }); + + // Color code by movement type + const typeColors: Record = { + purchase: 'FF92D050', + sale: 'FFFFC000', + adjustment: 'FF00B0F0', + loss: 'FFFF0000', + return: 'FF00B050', + transfer: 'FFFFA500', + }; + + const color = typeColors[movement.movementType] || 'FFFFFFFF'; + row.getCell('type').fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: color }, + }; + } + + sheet.getColumn('I').numFmt = '"$"#,##0.00'; + + // Summary by type + sheet.addRow([]); + const summaryRow = sheet.addRow(['Resumen por Tipo']); + summaryRow.font = { bold: true }; + + const typeGroups = this.groupByMovementType(movements); + Object.entries(typeGroups).forEach(([type, items]) => { + sheet.addRow([this.getMovementTypeLabel(type), items.length]); + }); + + const arrayBuffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(arrayBuffer); + } + + // ============ HELPER METHODS ============ + + private getDateRangeText(startDate?: string, endDate?: string): string { + if (startDate && endDate) { + return `Periodo: ${startDate} - ${endDate}`; + } else if (startDate) { + return `Desde: ${startDate}`; + } else if (endDate) { + return `Hasta: ${endDate}`; + } + return 'Todos los registros'; + } + + private getStatusLabel(status: SaleStatus): string { + const labels: Record = { + [SaleStatus.COMPLETED]: 'Completada', + [SaleStatus.CANCELLED]: 'Cancelada', + [SaleStatus.REFUNDED]: 'Reembolsada', + }; + return labels[status] || status; + } + + private getFiadoStatusLabel(status: FiadoStatus): string { + const labels: Record = { + [FiadoStatus.PENDING]: 'Pendiente', + [FiadoStatus.PARTIAL]: 'Pago Parcial', + [FiadoStatus.PAID]: 'Pagado', + [FiadoStatus.CANCELLED]: 'Cancelado', + }; + return labels[status] || status; + } + + private getMovementTypeLabel(type: string): string { + const labels: Record = { + purchase: 'Compra', + sale: 'Venta', + adjustment: 'Ajuste', + loss: 'Pérdida', + return: 'Devolución', + transfer: 'Transferencia', + }; + return labels[type] || type; + } + + private groupByMovementType(movements: InventoryMovement[]): Record { + return movements.reduce((groups, movement) => { + const type = movement.movementType; + if (!groups[type]) { + groups[type] = []; + } + groups[type].push(movement); + return groups; + }, {} as Record); + } +}