Major update: ESG, KPIs, metas, alertas, auditoria, documentos, importação, relatórios, subcategorias, dashboard orçamentos
This commit is contained in:
346
backend/package-lock.json
generated
346
backend/package-lock.json
generated
@@ -15,16 +15,19 @@
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"multer": "^2.0.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.18.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.28",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -33,8 +36,7 @@
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
@@ -2767,21 +2769,10 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
@@ -2792,7 +2783,6 @@
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -2838,7 +2828,6 @@
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -2850,7 +2839,6 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
|
||||
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -2863,7 +2851,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
@@ -2934,6 +2921,15 @@
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/multer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
|
||||
"integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz",
|
||||
@@ -2979,21 +2975,18 @@
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -3003,7 +2996,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
||||
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -3840,6 +3832,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"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/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -4210,6 +4211,8 @@
|
||||
"integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
@@ -4223,6 +4226,8 @@
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
@@ -4231,6 +4236,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
@@ -4347,6 +4353,7 @@
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4487,6 +4494,19 @@
|
||||
],
|
||||
"license": "CC-BY-4.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/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -4541,7 +4561,9 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.4",
|
||||
@@ -4697,6 +4719,15 @@
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"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/collect-v8-coverage": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
|
||||
@@ -4906,6 +4937,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@@ -4955,6 +4998,8 @@
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
@@ -4984,6 +5029,8 @@
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
@@ -5059,6 +5106,8 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -5181,6 +5230,8 @@
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
@@ -5570,6 +5621,8 @@
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -5749,7 +5802,9 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
@@ -5949,6 +6004,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
@@ -5962,7 +6026,9 @@
|
||||
"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"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
@@ -6100,7 +6166,9 @@
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.0",
|
||||
@@ -6433,7 +6501,9 @@
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
@@ -7930,6 +8000,8 @@
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -7984,7 +8056,9 @@
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
@@ -8067,7 +8141,9 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.3.4",
|
||||
@@ -8113,6 +8189,8 @@
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
|
||||
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
@@ -8480,6 +8558,95 @@
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
||||
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.11.0",
|
||||
"pg-pool": "^3.11.0",
|
||||
"pg-protocol": "^1.11.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
||||
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
|
||||
"integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
|
||||
"integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
|
||||
"integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -8598,11 +8765,52 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
@@ -8709,6 +8917,8 @@
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
@@ -8795,6 +9005,8 @@
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
@@ -8810,6 +9022,8 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9231,7 +9445,9 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
@@ -9252,6 +9468,8 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -9299,6 +9517,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -9322,6 +9549,18 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"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/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@@ -9582,6 +9821,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
@@ -9594,6 +9835,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
@@ -10096,6 +10339,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
@@ -10895,6 +11140,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"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/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
@@ -10965,6 +11228,27 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"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/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -26,16 +26,19 @@
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"multer": "^2.0.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.18.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.28",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^13.0.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -44,8 +47,7 @@
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
|
||||
@@ -7,11 +7,20 @@ import { CentrosCustoModule } from './modules/centros-custo/centros-custo.module
|
||||
import { CategoriasModule } from './modules/categorias/categorias.module';
|
||||
import { FornecedoresModule } from './modules/fornecedores/fornecedores.module';
|
||||
import { DemandasModule } from './modules/demandas/demandas.module';
|
||||
import { SubcategoriasModule } from './modules/subcategorias/subcategorias.module';
|
||||
import { DocumentosModule } from './modules/documentos/documentos.module';
|
||||
import { PropostasModule } from './modules/propostas/propostas.module';
|
||||
import { OrcamentoModule } from './modules/orcamento/orcamento.module';
|
||||
import { WorkflowModule } from './modules/workflow/workflow.module';
|
||||
import { DashboardModule } from './modules/dashboard/dashboard.module';
|
||||
import { OrdensServicoModule } from './modules/ordens-servico/ordens-servico.module';
|
||||
import { EsgModule } from './modules/esg/esg.module';
|
||||
import { KpisModule } from './modules/kpis/kpis.module';
|
||||
import { AuditModule } from './modules/audit/audit.module';
|
||||
import { ImportModule } from './modules/import/import.module';
|
||||
import { RelatoriosModule } from './modules/relatorios/relatorios.module';
|
||||
import { MetasModule } from './modules/metas/metas.module';
|
||||
import { AlertasModule } from './modules/alertas/alertas.module';
|
||||
import { SeedService } from './database/seeds/seed.service';
|
||||
import { Perfil } from './modules/users/entities/perfil.entity';
|
||||
import { Usuario } from './modules/users/entities/usuario.entity';
|
||||
@@ -22,26 +31,39 @@ import { Fornecedor } from './modules/fornecedores/entities/fornecedor.entity';
|
||||
import { Certidao } from './modules/fornecedores/entities/certidao.entity';
|
||||
import { Demanda } from './modules/demandas/entities/demanda.entity';
|
||||
import { ItemLinha } from './modules/demandas/entities/item-linha.entity';
|
||||
import { Subcategoria } from './modules/subcategorias/entities/subcategoria.entity';
|
||||
import { Documento } from './modules/documentos/entities/documento.entity';
|
||||
import { Proposta } from './modules/propostas/entities/proposta.entity';
|
||||
import { OrcamentoPlanejado } from './modules/orcamento/entities/orcamento-planejado.entity';
|
||||
import { WorkflowAprovacao } from './modules/workflow/entities/workflow-aprovacao.entity';
|
||||
import { OrdemServico } from './modules/ordens-servico/entities/ordem-servico.entity';
|
||||
import { Avaliacao } from './modules/ordens-servico/entities/avaliacao.entity';
|
||||
import { DocumentoVersao } from './modules/ordens-servico/entities/documento-versao.entity';
|
||||
import { Alerta } from './modules/dashboard/entities/alerta.entity';
|
||||
import { AuditLog } from './modules/dashboard/entities/audit-log.entity';
|
||||
import { EsgMetrica } from './modules/esg/entities/esg-metrica.entity';
|
||||
import { EsgMeta } from './modules/esg/entities/esg-meta.entity';
|
||||
import { Kpi } from './modules/kpis/entities/kpi.entity';
|
||||
import { Meta } from './modules/metas/entities/meta.entity';
|
||||
import { AlertaConfig } from './modules/alertas/entities/alerta-config.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'better-sqlite3',
|
||||
database: 'hefesto.db',
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
username: process.env.DB_USERNAME || 'hefesto',
|
||||
password: process.env.DB_PASSWORD || 'Hefesto2026!',
|
||||
database: process.env.DB_DATABASE || 'hefesto',
|
||||
autoLoadEntities: true,
|
||||
synchronize: true,
|
||||
}),
|
||||
TypeOrmModule.forFeature([
|
||||
Perfil, Usuario, Local, CentroCusto, Categoria, Fornecedor, Certidao,
|
||||
Demanda, ItemLinha, Proposta, OrcamentoPlanejado, WorkflowAprovacao,
|
||||
OrdemServico, Avaliacao, Alerta, AuditLog,
|
||||
Demanda, ItemLinha, Subcategoria, Documento, Proposta, OrcamentoPlanejado, WorkflowAprovacao,
|
||||
OrdemServico, Avaliacao, DocumentoVersao, Alerta, AuditLog,
|
||||
EsgMetrica, EsgMeta, Kpi, Meta, AlertaConfig,
|
||||
]),
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
@@ -50,11 +72,20 @@ import { AuditLog } from './modules/dashboard/entities/audit-log.entity';
|
||||
CategoriasModule,
|
||||
FornecedoresModule,
|
||||
DemandasModule,
|
||||
SubcategoriasModule,
|
||||
DocumentosModule,
|
||||
PropostasModule,
|
||||
OrcamentoModule,
|
||||
WorkflowModule,
|
||||
DashboardModule,
|
||||
OrdensServicoModule,
|
||||
EsgModule,
|
||||
KpisModule,
|
||||
AuditModule,
|
||||
ImportModule,
|
||||
RelatoriosModule,
|
||||
MetasModule,
|
||||
AlertasModule,
|
||||
],
|
||||
providers: [SeedService],
|
||||
})
|
||||
|
||||
@@ -17,6 +17,12 @@ import { OrcamentoPlanejado } from '../../modules/orcamento/entities/orcamento-p
|
||||
import { WorkflowAprovacao } from '../../modules/workflow/entities/workflow-aprovacao.entity';
|
||||
import { OrdemServico } from '../../modules/ordens-servico/entities/ordem-servico.entity';
|
||||
import { Alerta } from '../../modules/dashboard/entities/alerta.entity';
|
||||
import { EsgMetrica } from '../../modules/esg/entities/esg-metrica.entity';
|
||||
import { EsgMeta } from '../../modules/esg/entities/esg-meta.entity';
|
||||
import { Kpi } from '../../modules/kpis/entities/kpi.entity';
|
||||
import { Meta } from '../../modules/metas/entities/meta.entity';
|
||||
import { AlertaConfig } from '../../modules/alertas/entities/alerta-config.entity';
|
||||
import { Subcategoria } from '../../modules/subcategorias/entities/subcategoria.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SeedService {
|
||||
@@ -35,6 +41,12 @@ export class SeedService {
|
||||
@InjectRepository(WorkflowAprovacao) private wfRepo: Repository<WorkflowAprovacao>,
|
||||
@InjectRepository(OrdemServico) private osRepo: Repository<OrdemServico>,
|
||||
@InjectRepository(Alerta) private alertaRepo: Repository<Alerta>,
|
||||
@InjectRepository(EsgMetrica) private esgMetricaRepo: Repository<EsgMetrica>,
|
||||
@InjectRepository(EsgMeta) private esgMetaRepo: Repository<EsgMeta>,
|
||||
@InjectRepository(Kpi) private kpiRepo: Repository<Kpi>,
|
||||
@InjectRepository(Meta) private metaRepo: Repository<Meta>,
|
||||
@InjectRepository(AlertaConfig) private alertaConfigRepo: Repository<AlertaConfig>,
|
||||
@InjectRepository(Subcategoria) private subcatRepo: Repository<Subcategoria>,
|
||||
) {}
|
||||
|
||||
async seed() {
|
||||
@@ -82,34 +94,63 @@ export class SeedService {
|
||||
|
||||
// Locais
|
||||
const locais = [
|
||||
{ id: uuid(), nome: 'Torre Norte - SP', endereco: 'Av. Paulista, 1000, São Paulo - SP', centro_custo_id: ccs[0].id, responsavel_id: users[2].id },
|
||||
{ id: uuid(), nome: 'Torre Sul - SP', endereco: 'Av. Paulista, 1002, São Paulo - SP', centro_custo_id: ccs[0].id, responsavel_id: users[2].id },
|
||||
{ id: uuid(), nome: 'Escritório RJ', endereco: 'Rua do Ouvidor, 50, Rio de Janeiro - RJ', centro_custo_id: ccs[1].id, responsavel_id: users[6].id },
|
||||
{ id: uuid(), nome: 'CD Betim', endereco: 'Rod. Fernão Dias, km 492, Betim - MG', centro_custo_id: ccs[2].id, responsavel_id: users[2].id },
|
||||
{ id: uuid(), nome: 'Fábrica Campinas', endereco: 'Distrito Industrial, Campinas - SP', centro_custo_id: ccs[4].id, responsavel_id: users[6].id },
|
||||
{ id: uuid(), nome: 'Torre Norte - SP', endereco: 'Av. Paulista, 1000, São Paulo - SP', centro_custo_id: ccs[0].id, responsavel_id: users[2].id, tipo_operacao_local: 'Administrativo', classificacao_impacto_ambiental: 'Baixo', praticas_sustentaveis: ['Coleta Seletiva', 'Energia Renovável'] },
|
||||
{ id: uuid(), nome: 'Torre Sul - SP', endereco: 'Av. Paulista, 1002, São Paulo - SP', centro_custo_id: ccs[0].id, responsavel_id: users[2].id, tipo_operacao_local: 'Administrativo', classificacao_impacto_ambiental: 'Baixo', praticas_sustentaveis: ['Coleta Seletiva'] },
|
||||
{ id: uuid(), nome: 'Escritório RJ', endereco: 'Rua do Ouvidor, 50, Rio de Janeiro - RJ', centro_custo_id: ccs[1].id, responsavel_id: users[6].id, tipo_operacao_local: 'Comercial', classificacao_impacto_ambiental: 'Médio', praticas_sustentaveis: ['Reuso de Água'] },
|
||||
{ id: uuid(), nome: 'CD Betim', endereco: 'Rod. Fernão Dias, km 492, Betim - MG', centro_custo_id: ccs[2].id, responsavel_id: users[2].id, tipo_operacao_local: 'Logístico', classificacao_impacto_ambiental: 'Alto', praticas_sustentaveis: ['Coleta Seletiva', 'Reuso de Água'] },
|
||||
{ id: uuid(), nome: 'Fábrica Campinas', endereco: 'Distrito Industrial, Campinas - SP', centro_custo_id: ccs[4].id, responsavel_id: users[6].id, tipo_operacao_local: 'Industrial', classificacao_impacto_ambiental: 'Alto', praticas_sustentaveis: ['Coleta Seletiva', 'Reuso de Água', 'Energia Renovável'] },
|
||||
];
|
||||
await this.localRepo.save(locais);
|
||||
|
||||
// Categorias
|
||||
const cats = [
|
||||
{ id: uuid(), nome: 'Manutenção Predial', criticidade_padrao: 'media', sla_dias: 15 },
|
||||
{ id: uuid(), nome: 'Climatização e HVAC', criticidade_padrao: 'alta', sla_dias: 7 },
|
||||
{ id: uuid(), nome: 'Limpeza e Conservação', criticidade_padrao: 'baixa', sla_dias: 30 },
|
||||
{ id: uuid(), nome: 'Segurança Patrimonial', criticidade_padrao: 'critica', sla_dias: 3 },
|
||||
{ id: uuid(), nome: 'Elétrica e Iluminação', criticidade_padrao: 'alta', sla_dias: 10 },
|
||||
{ id: uuid(), nome: 'Paisagismo', criticidade_padrao: 'baixa', sla_dias: 45 },
|
||||
{ id: uuid(), nome: 'Dedetização e Controle de Pragas', criticidade_padrao: 'media', sla_dias: 15 },
|
||||
{ id: uuid(), nome: 'Reforma e Adequação', criticidade_padrao: 'media', sla_dias: 60 },
|
||||
{ id: uuid(), nome: 'Manutenção Predial', criticidade_padrao: 'media', sla_dias: 15, tipo_manutencao: 'Preventiva', impacto_ambiental_esperado: 'Baixo', potencial_geracao_residuos: 'Baixo' },
|
||||
{ id: uuid(), nome: 'Climatização e HVAC', criticidade_padrao: 'alta', sla_dias: 7, tipo_manutencao: 'Preventiva', impacto_ambiental_esperado: 'Médio', potencial_geracao_residuos: 'Médio' },
|
||||
{ id: uuid(), nome: 'Limpeza e Conservação', criticidade_padrao: 'baixa', sla_dias: 30, tipo_manutencao: 'Preventiva', impacto_ambiental_esperado: 'Baixo', potencial_geracao_residuos: 'Médio' },
|
||||
{ id: uuid(), nome: 'Segurança Patrimonial', criticidade_padrao: 'critica', sla_dias: 3, tipo_manutencao: 'Emergencial', impacto_ambiental_esperado: 'Baixo', potencial_geracao_residuos: 'Baixo' },
|
||||
{ id: uuid(), nome: 'Elétrica e Iluminação', criticidade_padrao: 'alta', sla_dias: 10, tipo_manutencao: 'Corretiva', impacto_ambiental_esperado: 'Alto', potencial_geracao_residuos: 'Alto' },
|
||||
{ id: uuid(), nome: 'Paisagismo', criticidade_padrao: 'baixa', sla_dias: 45, tipo_manutencao: 'Preventiva', impacto_ambiental_esperado: 'Baixo', potencial_geracao_residuos: 'Baixo' },
|
||||
{ id: uuid(), nome: 'Dedetização e Controle de Pragas', criticidade_padrao: 'media', sla_dias: 15, tipo_manutencao: 'Preventiva', impacto_ambiental_esperado: 'Alto', potencial_geracao_residuos: 'Médio' },
|
||||
{ id: uuid(), nome: 'Reforma e Adequação', criticidade_padrao: 'media', sla_dias: 60, tipo_manutencao: 'Corretiva', impacto_ambiental_esperado: 'Alto', potencial_geracao_residuos: 'Alto' },
|
||||
];
|
||||
await this.catRepo.save(cats);
|
||||
|
||||
// Subcategorias
|
||||
const subcats = [
|
||||
{ id: uuid(), nome: 'Preventiva', categoria_id: cats[0].id },
|
||||
{ id: uuid(), nome: 'Corretiva', categoria_id: cats[0].id },
|
||||
{ id: uuid(), nome: 'Preditiva', categoria_id: cats[0].id },
|
||||
{ id: uuid(), nome: 'Split', categoria_id: cats[1].id },
|
||||
{ id: uuid(), nome: 'Central', categoria_id: cats[1].id },
|
||||
{ id: uuid(), nome: 'Ventilação', categoria_id: cats[1].id },
|
||||
{ id: uuid(), nome: 'Diária', categoria_id: cats[2].id },
|
||||
{ id: uuid(), nome: 'Pesada', categoria_id: cats[2].id },
|
||||
{ id: uuid(), nome: 'Pós-obra', categoria_id: cats[2].id },
|
||||
{ id: uuid(), nome: 'CFTV', categoria_id: cats[3].id },
|
||||
{ id: uuid(), nome: 'Controle de Acesso', categoria_id: cats[3].id },
|
||||
{ id: uuid(), nome: 'Vigilância', categoria_id: cats[3].id },
|
||||
{ id: uuid(), nome: 'Quadros', categoria_id: cats[4].id },
|
||||
{ id: uuid(), nome: 'Iluminação', categoria_id: cats[4].id },
|
||||
{ id: uuid(), nome: 'Cabeamento', categoria_id: cats[4].id },
|
||||
{ id: uuid(), nome: 'Poda', categoria_id: cats[5].id },
|
||||
{ id: uuid(), nome: 'Jardinagem', categoria_id: cats[5].id },
|
||||
{ id: uuid(), nome: 'Irrigação', categoria_id: cats[5].id },
|
||||
{ id: uuid(), nome: 'Insetos', categoria_id: cats[6].id },
|
||||
{ id: uuid(), nome: 'Roedores', categoria_id: cats[6].id },
|
||||
{ id: uuid(), nome: 'Sanitização', categoria_id: cats[6].id },
|
||||
{ id: uuid(), nome: 'Pintura', categoria_id: cats[7].id },
|
||||
{ id: uuid(), nome: 'Piso', categoria_id: cats[7].id },
|
||||
{ id: uuid(), nome: 'Alvenaria', categoria_id: cats[7].id },
|
||||
];
|
||||
await this.subcatRepo.save(subcats);
|
||||
|
||||
// Fornecedores
|
||||
const forns = [
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '12.345.678/0001-90', razao_social: 'TechClima Engenharia Ltda', nome_fantasia: 'TechClima', email: 'contato@techclima.com.br', telefone: '(11) 3456-7890', rating: 4.5, usuario_id: users[7].id, categorias_atendidas: [cats[1].id] },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '23.456.789/0001-01', razao_social: 'ServiLimp Serviços de Limpeza S/A', nome_fantasia: 'ServiLimp', email: 'comercial@servilimp.com.br', telefone: '(11) 2345-6789', rating: 3.8, usuario_id: users[8].id, categorias_atendidas: [cats[2].id] },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '34.567.890/0001-12', razao_social: 'Forte Segurança Empresarial Ltda', nome_fantasia: 'Forte Seg', email: 'propostas@forteseg.com.br', telefone: '(21) 3456-7890', rating: 4.2, usuario_id: users[9].id, categorias_atendidas: [cats[3].id] },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '45.678.901/0001-23', razao_social: 'EletroForce Instalações Elétricas', nome_fantasia: 'EletroForce', email: 'orcamento@eletroforce.com.br', telefone: '(11) 4567-8901', rating: 4.0, categorias_atendidas: [cats[4].id] },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '56.789.012/0001-34', razao_social: 'Predial Master Engenharia', nome_fantasia: 'Predial Master', email: 'contato@predialmaster.com.br', telefone: '(11) 5678-9012', rating: 4.7, categorias_atendidas: [cats[0].id, cats[7].id] },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '12.345.678/0001-90', razao_social: 'TechClima Engenharia Ltda', nome_fantasia: 'TechClima', email: 'contato@techclima.com.br', telefone: '(11) 3456-7890', rating: 4.5, usuario_id: users[7].id, categorias_atendidas: [cats[1].id], possui_politica_ambiental: true, possui_politica_sst: true, declara_uso_epi: true, equipe_treinada: true, classificacao_esg: 'Avançado' },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '23.456.789/0001-01', razao_social: 'ServiLimp Serviços de Limpeza S/A', nome_fantasia: 'ServiLimp', email: 'comercial@servilimp.com.br', telefone: '(11) 2345-6789', rating: 3.8, usuario_id: users[8].id, categorias_atendidas: [cats[2].id], possui_politica_ambiental: false, possui_politica_sst: true, declara_uso_epi: true, equipe_treinada: false, classificacao_esg: 'Básico' },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '34.567.890/0001-12', razao_social: 'Forte Segurança Empresarial Ltda', nome_fantasia: 'Forte Seg', email: 'propostas@forteseg.com.br', telefone: '(21) 3456-7890', rating: 4.2, usuario_id: users[9].id, categorias_atendidas: [cats[3].id], possui_politica_ambiental: true, possui_politica_sst: true, declara_uso_epi: true, equipe_treinada: true, classificacao_esg: 'Intermediário' },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '45.678.901/0001-23', razao_social: 'EletroForce Instalações Elétricas', nome_fantasia: 'EletroForce', email: 'orcamento@eletroforce.com.br', telefone: '(11) 4567-8901', rating: 4.0, categorias_atendidas: [cats[4].id], possui_politica_ambiental: true, possui_politica_sst: false, declara_uso_epi: true, equipe_treinada: true, classificacao_esg: 'Intermediário' },
|
||||
{ id: uuid(), tipo_pessoa: 'PJ', cpf_cnpj: '56.789.012/0001-34', razao_social: 'Predial Master Engenharia', nome_fantasia: 'Predial Master', email: 'contato@predialmaster.com.br', telefone: '(11) 5678-9012', rating: 4.7, categorias_atendidas: [cats[0].id, cats[7].id], possui_politica_ambiental: true, possui_politica_sst: true, declara_uso_epi: true, equipe_treinada: true, classificacao_esg: 'Avançado' },
|
||||
];
|
||||
await this.fornRepo.save(forns);
|
||||
|
||||
@@ -128,16 +169,16 @@ export class SeedService {
|
||||
|
||||
// Demandas
|
||||
const demandas = [
|
||||
{ id: uuid(), numero: 1, titulo: 'Manutenção preventiva ar-condicionado Torre Norte', descricao: 'Revisão semestral de todos os 48 splits e 6 centrais de ar', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[1].id, criticidade: 'alta', status: 'em_cotacao', solicitante_id: users[1].id, gestor_id: users[2].id, data_desejada: '2026-03-15' },
|
||||
{ id: uuid(), numero: 2, titulo: 'Contratação de serviço de limpeza - Escritório RJ', descricao: 'Limpeza diária do escritório RJ - 2 pavimentos, 800m²', local_id: locais[2].id, centro_custo_id: ccs[1].id, categoria_id: cats[2].id, criticidade: 'media', status: 'propostas_recebidas', solicitante_id: users[5].id, gestor_id: users[6].id },
|
||||
{ id: uuid(), numero: 3, titulo: 'Instalação de câmeras de segurança - CD Betim', descricao: 'Instalação de 32 câmeras IP + NVR + cabeamento', local_id: locais[3].id, centro_custo_id: ccs[2].id, categoria_id: cats[3].id, criticidade: 'critica', status: 'em_aprovacao', solicitante_id: users[1].id, gestor_id: users[2].id },
|
||||
{ id: uuid(), numero: 4, titulo: 'Troca de iluminação para LED - Torre Sul', descricao: 'Substituição de 500 lâmpadas fluorescentes por LED', local_id: locais[1].id, centro_custo_id: ccs[0].id, categoria_id: cats[4].id, criticidade: 'media', status: 'aprovada', solicitante_id: users[5].id, gestor_id: users[2].id },
|
||||
{ id: uuid(), numero: 5, titulo: 'Reforma do refeitório - Fábrica Campinas', descricao: 'Reforma completa: piso, pintura, bancadas, instalações', local_id: locais[4].id, centro_custo_id: ccs[4].id, categoria_id: cats[7].id, criticidade: 'media', status: 'em_execucao', solicitante_id: users[1].id, gestor_id: users[6].id },
|
||||
{ id: uuid(), numero: 6, titulo: 'Dedetização trimestral - Todas as unidades', descricao: 'Controle de pragas urbanas em todas as 5 unidades', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[6].id, criticidade: 'baixa', status: 'aberta', solicitante_id: users[5].id },
|
||||
{ id: uuid(), numero: 7, titulo: 'Reparo no telhado - CD Betim', descricao: 'Vazamento em 3 pontos do galpão principal', local_id: locais[3].id, centro_custo_id: ccs[2].id, categoria_id: cats[0].id, criticidade: 'alta', status: 'em_escopo', solicitante_id: users[1].id, gestor_id: users[2].id },
|
||||
{ id: uuid(), numero: 8, titulo: 'Manutenção do paisagismo - Sede SP', descricao: 'Poda, jardinagem e manutenção das áreas verdes', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[5].id, criticidade: 'baixa', status: 'rascunho', solicitante_id: users[5].id },
|
||||
{ id: uuid(), numero: 9, titulo: 'Instalação de gerador de emergência - Torre Norte', descricao: 'Gerador diesel 500kVA com QTA', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[4].id, criticidade: 'critica', status: 'concluida', solicitante_id: users[1].id, gestor_id: users[2].id },
|
||||
{ id: uuid(), numero: 10, titulo: 'Adequação NR-10 - Fábrica Campinas', descricao: 'Adequação de quadros e instalações elétricas à NR-10', local_id: locais[4].id, centro_custo_id: ccs[4].id, categoria_id: cats[4].id, criticidade: 'alta', status: 'cancelada', solicitante_id: users[5].id, gestor_id: users[6].id },
|
||||
{ id: uuid(), numero: 1, titulo: 'Manutenção preventiva ar-condicionado Torre Norte', descricao: 'Revisão semestral de todos os 48 splits e 6 centrais de ar', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[1].id, criticidade: 'alta', status: 'em_cotacao', solicitante_id: users[1].id, gestor_id: users[2].id, data_desejada: '2026-03-15', impacto_ambiental_demanda: 'Médio' },
|
||||
{ id: uuid(), numero: 2, titulo: 'Contratação de serviço de limpeza - Escritório RJ', descricao: 'Limpeza diária do escritório RJ - 2 pavimentos, 800m²', local_id: locais[2].id, centro_custo_id: ccs[1].id, categoria_id: cats[2].id, criticidade: 'media', status: 'propostas_recebidas', solicitante_id: users[5].id, gestor_id: users[6].id, impacto_ambiental_demanda: 'Baixo' },
|
||||
{ id: uuid(), numero: 3, titulo: 'Instalação de câmeras de segurança - CD Betim', descricao: 'Instalação de 32 câmeras IP + NVR + cabeamento', local_id: locais[3].id, centro_custo_id: ccs[2].id, categoria_id: cats[3].id, criticidade: 'critica', status: 'em_aprovacao', solicitante_id: users[1].id, gestor_id: users[2].id, impacto_ambiental_demanda: 'Baixo', justificativa_manutencao_emergencial: 'Falha no sistema de segurança existente, risco patrimonial' },
|
||||
{ id: uuid(), numero: 4, titulo: 'Troca de iluminação para LED - Torre Sul', descricao: 'Substituição de 500 lâmpadas fluorescentes por LED', local_id: locais[1].id, centro_custo_id: ccs[0].id, categoria_id: cats[4].id, criticidade: 'media', status: 'aprovada', solicitante_id: users[5].id, gestor_id: users[2].id, impacto_ambiental_demanda: 'Alto' },
|
||||
{ id: uuid(), numero: 5, titulo: 'Reforma do refeitório - Fábrica Campinas', descricao: 'Reforma completa: piso, pintura, bancadas, instalações', local_id: locais[4].id, centro_custo_id: ccs[4].id, categoria_id: cats[7].id, criticidade: 'media', status: 'em_execucao', solicitante_id: users[1].id, gestor_id: users[6].id, impacto_ambiental_demanda: 'Alto' },
|
||||
{ id: uuid(), numero: 6, titulo: 'Dedetização trimestral - Todas as unidades', descricao: 'Controle de pragas urbanas em todas as 5 unidades', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[6].id, criticidade: 'baixa', status: 'aberta', solicitante_id: users[5].id, impacto_ambiental_demanda: 'Alto' },
|
||||
{ id: uuid(), numero: 7, titulo: 'Reparo no telhado - CD Betim', descricao: 'Vazamento em 3 pontos do galpão principal', local_id: locais[3].id, centro_custo_id: ccs[2].id, categoria_id: cats[0].id, criticidade: 'alta', status: 'em_escopo', solicitante_id: users[1].id, gestor_id: users[2].id, impacto_ambiental_demanda: 'Baixo' },
|
||||
{ id: uuid(), numero: 8, titulo: 'Manutenção do paisagismo - Sede SP', descricao: 'Poda, jardinagem e manutenção das áreas verdes', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[5].id, criticidade: 'baixa', status: 'rascunho', solicitante_id: users[5].id, impacto_ambiental_demanda: 'Baixo' },
|
||||
{ id: uuid(), numero: 9, titulo: 'Instalação de gerador de emergência - Torre Norte', descricao: 'Gerador diesel 500kVA com QTA', local_id: locais[0].id, centro_custo_id: ccs[0].id, categoria_id: cats[4].id, criticidade: 'critica', status: 'concluida', solicitante_id: users[1].id, gestor_id: users[2].id, impacto_ambiental_demanda: 'Alto' },
|
||||
{ id: uuid(), numero: 10, titulo: 'Adequação NR-10 - Fábrica Campinas', descricao: 'Adequação de quadros e instalações elétricas à NR-10', local_id: locais[4].id, centro_custo_id: ccs[4].id, categoria_id: cats[4].id, criticidade: 'alta', status: 'cancelada', solicitante_id: users[5].id, gestor_id: users[6].id, impacto_ambiental_demanda: 'Médio' },
|
||||
];
|
||||
await this.demandaRepo.save(demandas);
|
||||
|
||||
@@ -244,6 +285,71 @@ export class SeedService {
|
||||
];
|
||||
await this.alertaRepo.save(alertas);
|
||||
|
||||
// ESG Métricas
|
||||
const esgMetricas = [];
|
||||
const tiposEsg = ['energia', 'agua', 'residuos', 'emissoes'];
|
||||
const unidades = { energia: 'kWh', agua: 'm³', residuos: 'kg', emissoes: 'tCO2e' };
|
||||
for (let mes = 1; mes <= 12; mes++) {
|
||||
for (const tipo of tiposEsg) {
|
||||
esgMetricas.push({
|
||||
id: uuid(), tipo, unidade_medida: unidades[tipo],
|
||||
valor: Math.round((tipo === 'energia' ? 15000 + Math.random() * 10000 : tipo === 'agua' ? 500 + Math.random() * 300 : tipo === 'residuos' ? 2000 + Math.random() * 1500 : 50 + Math.random() * 30) * 100) / 100,
|
||||
periodo_mes: mes, periodo_ano: 2026, local_id: locais[0].id, centro_custo_id: ccs[0].id,
|
||||
fonte_dados: 'Medidor automático', observacoes: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.esgMetricaRepo.save(esgMetricas);
|
||||
|
||||
// ESG Metas
|
||||
const esgMetas = [
|
||||
{ id: uuid(), tipo: 'energia', meta_valor: 200000, periodo_ano: 2026, local_id: locais[0].id },
|
||||
{ id: uuid(), tipo: 'agua', meta_valor: 8000, periodo_ano: 2026, local_id: locais[0].id },
|
||||
{ id: uuid(), tipo: 'residuos', meta_valor: 30000, periodo_ano: 2026, local_id: locais[0].id },
|
||||
{ id: uuid(), tipo: 'emissoes', meta_valor: 600, periodo_ano: 2026, local_id: locais[0].id },
|
||||
{ id: uuid(), tipo: 'energia', meta_valor: 100000, periodo_ano: 2026, local_id: locais[2].id },
|
||||
];
|
||||
await this.esgMetaRepo.save(esgMetas);
|
||||
|
||||
// KPIs
|
||||
const kpis = [
|
||||
{ id: uuid(), nome: 'Custo por m²', descricao: 'Custo de facilities por metro quadrado', formula: 'custo_total / area_total', categoria: 'financeiro', unidade: 'R$/m²', meta_valor: 45, valor_atual: 38.5, periodo_mes: 1, periodo_ano: 2026, status: 'verde' },
|
||||
{ id: uuid(), nome: 'SLA de Atendimento', descricao: '% de demandas atendidas dentro do SLA', formula: 'demandas_no_prazo / total_demandas * 100', categoria: 'prazo', unidade: '%', meta_valor: 90, valor_atual: 82, periodo_mes: 1, periodo_ano: 2026, status: 'amarelo' },
|
||||
{ id: uuid(), nome: 'Índice de Retrabalho', descricao: '% de OS com retrabalho', formula: 'os_retrabalho / total_os * 100', categoria: 'qualidade', unidade: '%', meta_valor: 5, valor_atual: 3.2, periodo_mes: 1, periodo_ano: 2026, status: 'verde' },
|
||||
{ id: uuid(), nome: 'Ocupação de Espaços', descricao: '% de ocupação dos espaços', formula: 'area_ocupada / area_total * 100', categoria: 'operacional', unidade: '%', meta_valor: 85, valor_atual: 78, periodo_mes: 1, periodo_ano: 2026, status: 'amarelo' },
|
||||
{ id: uuid(), nome: 'Economia Energética', descricao: '% de redução no consumo de energia', formula: '(consumo_anterior - consumo_atual) / consumo_anterior * 100', categoria: 'financeiro', unidade: '%', meta_valor: 10, valor_atual: 7.5, periodo_mes: 1, periodo_ano: 2026, status: 'amarelo' },
|
||||
{ id: uuid(), nome: 'Satisfação do Cliente Interno', descricao: 'Nota média das avaliações', formula: 'soma_notas / total_avaliacoes', categoria: 'qualidade', unidade: 'nota', meta_valor: 4.5, valor_atual: 4.2, periodo_mes: 1, periodo_ano: 2026, status: 'verde' },
|
||||
{ id: uuid(), nome: 'Taxa de Manutenção Preventiva', descricao: '% preventiva vs corretiva', formula: 'manutencao_preventiva / total_manutencao * 100', categoria: 'operacional', unidade: '%', meta_valor: 70, valor_atual: 55, periodo_mes: 1, periodo_ano: 2026, status: 'vermelho' },
|
||||
{ id: uuid(), nome: 'Tempo Médio de Resposta', descricao: 'Tempo médio para primeira resposta a demanda', formula: 'soma_tempos_resposta / total_demandas', categoria: 'prazo', unidade: 'horas', meta_valor: 4, valor_atual: 3.1, periodo_mes: 1, periodo_ano: 2026, status: 'verde' },
|
||||
];
|
||||
await this.kpiRepo.save(kpis);
|
||||
|
||||
// Metas
|
||||
const metas = [
|
||||
{ id: uuid(), titulo: 'Reduzir custo operacional em 15%', descricao: 'Redução geral de custos operacionais de facilities', tipo: 'orcamento', centro_custo_id: ccs[0].id, valor_meta: 15, valor_atual: 8.3, unidade: '%', prazo: '2026-12-31', status: 'em_andamento' },
|
||||
{ id: uuid(), titulo: 'Implementar programa de reciclagem', descricao: 'Implantar coleta seletiva em todas as unidades', tipo: 'esg', valor_meta: 5, valor_atual: 3, unidade: 'unidades', prazo: '2026-06-30', status: 'em_andamento' },
|
||||
{ id: uuid(), titulo: 'SLA 95% até junho', descricao: 'Atingir 95% de atendimento dentro do SLA', tipo: 'operacional', valor_meta: 95, valor_atual: 82, unidade: '%', prazo: '2026-06-30', status: 'em_andamento' },
|
||||
{ id: uuid(), titulo: 'Reduzir consumo de energia 10%', descricao: 'Meta anual de redução energética', tipo: 'esg', centro_custo_id: ccs[0].id, valor_meta: 10, valor_atual: 7.5, unidade: '%', prazo: '2026-12-31', status: 'em_andamento' },
|
||||
{ id: uuid(), titulo: 'Digitalizar 100% dos contratos', descricao: 'Todos os contratos de fornecedores digitalizados', tipo: 'operacional', valor_meta: 100, valor_atual: 100, unidade: '%', prazo: '2026-03-31', status: 'atingida' },
|
||||
{ id: uuid(), titulo: 'Certificação ISO 14001', descricao: 'Obter certificação ambiental ISO 14001', tipo: 'esg', valor_meta: 1, valor_atual: 0, unidade: 'certificação', prazo: '2026-09-30', status: 'em_andamento' },
|
||||
{ id: uuid(), titulo: 'Orçamento anual: desvio < 5%', descricao: 'Manter desvio orçamentário abaixo de 5%', tipo: 'orcamento', valor_meta: 5, valor_atual: 3.2, unidade: '%', prazo: '2026-12-31', status: 'em_andamento' },
|
||||
{ id: uuid(), titulo: 'Rating fornecedores > 4.0', descricao: 'Manter rating médio de fornecedores acima de 4.0', tipo: 'operacional', valor_meta: 4.0, valor_atual: 4.2, unidade: 'nota', prazo: '2026-12-31', status: 'atingida' },
|
||||
{ id: uuid(), titulo: 'Reduzir consumo de água 8%', descricao: 'Meta anual de economia hídrica', tipo: 'esg', centro_custo_id: ccs[0].id, valor_meta: 8, valor_atual: 2.1, unidade: '%', prazo: '2026-12-31', status: 'atrasada' },
|
||||
{ id: uuid(), titulo: 'Zerar OS atrasadas', descricao: 'Nenhuma OS com atraso superior a 7 dias', tipo: 'operacional', valor_meta: 0, valor_atual: 2, unidade: 'OS', prazo: '2026-06-30', status: 'atrasada' },
|
||||
];
|
||||
await this.metaRepo.save(metas);
|
||||
|
||||
// Alerta Configs
|
||||
const alertaConfigs = [
|
||||
{ id: uuid(), tipo: 'orcamento_excedido', centro_custo_id: ccs[0].id, limite_percentual: 80, ativo: true },
|
||||
{ id: uuid(), tipo: 'orcamento_excedido', centro_custo_id: ccs[1].id, limite_percentual: 85, ativo: true },
|
||||
{ id: uuid(), tipo: 'certidao_vencendo', limite_percentual: 30, ativo: true },
|
||||
{ id: uuid(), tipo: 'os_atrasada', limite_percentual: 7, ativo: true },
|
||||
{ id: uuid(), tipo: 'meta_risco', limite_percentual: 50, ativo: true },
|
||||
{ id: uuid(), tipo: 'orcamento_excedido', centro_custo_id: ccs[4].id, limite_percentual: 90, ativo: false },
|
||||
];
|
||||
await this.alertaConfigRepo.save(alertaConfigs);
|
||||
|
||||
console.log('✅ Seed completed!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ async function bootstrap() {
|
||||
await seedService.seed();
|
||||
|
||||
await app.listen(8080);
|
||||
console.log('HEFESTO API running on http://localhost:8080');
|
||||
console.log('Nexus Facilities API running on http://localhost:8080');
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
16
backend/src/modules/alertas/alertas.controller.ts
Normal file
16
backend/src/modules/alertas/alertas.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get, Post, Body } from '@nestjs/common';
|
||||
import { AlertasService } from './alertas.service';
|
||||
|
||||
@Controller('alertas')
|
||||
export class AlertasController {
|
||||
constructor(private readonly svc: AlertasService) {}
|
||||
|
||||
@Get('configs')
|
||||
getConfigs() { return this.svc.getConfigs(); }
|
||||
|
||||
@Post('configurar')
|
||||
configurar(@Body() dto: any) { return this.svc.configurar(dto); }
|
||||
|
||||
@Post('verificar')
|
||||
verificar() { return this.svc.verificarAlertas(); }
|
||||
}
|
||||
14
backend/src/modules/alertas/alertas.module.ts
Normal file
14
backend/src/modules/alertas/alertas.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AlertaConfig } from './entities/alerta-config.entity';
|
||||
import { Alerta } from '../dashboard/entities/alerta.entity';
|
||||
import { AlertasController } from './alertas.controller';
|
||||
import { AlertasService } from './alertas.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AlertaConfig, Alerta])],
|
||||
controllers: [AlertasController],
|
||||
providers: [AlertasService],
|
||||
exports: [AlertasService],
|
||||
})
|
||||
export class AlertasModule {}
|
||||
67
backend/src/modules/alertas/alertas.service.ts
Normal file
67
backend/src/modules/alertas/alertas.service.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { AlertaConfig } from './entities/alerta-config.entity';
|
||||
import { Alerta } from '../dashboard/entities/alerta.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AlertasService {
|
||||
constructor(
|
||||
@InjectRepository(AlertaConfig) private configRepo: Repository<AlertaConfig>,
|
||||
@InjectRepository(Alerta) private alertaRepo: Repository<Alerta>,
|
||||
private ds: DataSource,
|
||||
) {}
|
||||
|
||||
async getConfigs() { return this.configRepo.find(); }
|
||||
|
||||
async configurar(dto: any) {
|
||||
return this.configRepo.save(this.configRepo.create(dto));
|
||||
}
|
||||
|
||||
async verificarAlertas() {
|
||||
const configs = await this.configRepo.find({ where: { ativo: true } });
|
||||
const gerados: any[] = [];
|
||||
|
||||
for (const cfg of configs) {
|
||||
if (cfg.tipo === 'orcamento_excedido') {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT centro_custo_id, SUM(valor_realizado) as realizado, SUM(valor_planejado) as planejado
|
||||
FROM orcamento_planejado
|
||||
${cfg.centro_custo_id ? 'WHERE centro_custo_id = ?' : ''}
|
||||
GROUP BY centro_custo_id
|
||||
HAVING (SUM(valor_realizado) / SUM(valor_planejado) * 100) > ?`,
|
||||
cfg.centro_custo_id ? [cfg.centro_custo_id, cfg.limite_percentual] : [cfg.limite_percentual]);
|
||||
for (const r of rows) {
|
||||
const pct = Math.round((r.realizado / r.planejado) * 100);
|
||||
gerados.push({ tipo: 'orcamento_excedido', titulo: `Orçamento excedeu ${pct}%`, mensagem: `Centro de custo ${r.centro_custo_id} atingiu ${pct}% do orçamento`, entidade: 'orcamento', usuario_id: 'system' });
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.tipo === 'certidao_vencendo') {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT c.*, f.razao_social FROM certidoes c
|
||||
LEFT JOIN fornecedores f ON f.id = c.fornecedor_id
|
||||
WHERE c.data_validade <= date('now', '+30 days') AND c.data_validade >= date('now')`);
|
||||
for (const r of rows) {
|
||||
gerados.push({ tipo: 'certidao_vencendo', titulo: `Certidão vencendo: ${r.tipo}`, mensagem: `Fornecedor ${r.razao_social} - certidão ${r.tipo} vence em ${r.data_validade}`, entidade: 'certidao', entidade_id: r.id, usuario_id: 'system' });
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.tipo === 'os_atrasada') {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT * FROM ordens_servico
|
||||
WHERE status NOT IN ('concluida','cancelada') AND data_conclusao IS NOT NULL AND data_conclusao < date('now')`);
|
||||
for (const r of rows) {
|
||||
gerados.push({ tipo: 'os_atrasada', titulo: `OS #${r.numero} atrasada`, mensagem: `Ordem de serviço #${r.numero} passou da data prevista`, entidade: 'ordem_servico', entidade_id: r.id, usuario_id: 'system' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save generated alerts
|
||||
for (const a of gerados) {
|
||||
await this.alertaRepo.save(this.alertaRepo.create(a));
|
||||
}
|
||||
|
||||
return { alertas_gerados: gerados.length, detalhes: gerados };
|
||||
}
|
||||
}
|
||||
22
backend/src/modules/alertas/entities/alerta-config.entity.ts
Normal file
22
backend/src/modules/alertas/entities/alerta-config.entity.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('alertas_config')
|
||||
export class AlertaConfig {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 50 })
|
||||
tipo: string; // orcamento_excedido | certidao_vencendo | os_atrasada | meta_risco
|
||||
|
||||
@Column({ nullable: true })
|
||||
centro_custo_id: string;
|
||||
|
||||
@Column({ type: 'float', default: 80 })
|
||||
limite_percentual: number;
|
||||
|
||||
@Column({ default: true })
|
||||
ativo: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
28
backend/src/modules/audit/audit.controller.ts
Normal file
28
backend/src/modules/audit/audit.controller.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Controller, Get, Query, Res } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
@Controller('audit')
|
||||
export class AuditController {
|
||||
constructor(private readonly svc: AuditService) {}
|
||||
|
||||
@Get('logs')
|
||||
logs(@Query() q: any) { return this.svc.logs(q); }
|
||||
|
||||
@Get('compliance-report')
|
||||
complianceReport(@Query() q: any) { return this.svc.complianceReport(q); }
|
||||
|
||||
@Get('export')
|
||||
async export(@Query() q: any, @Res() res: Response) {
|
||||
const format = q.format || 'json';
|
||||
const data = await this.svc.logs(q);
|
||||
if (format === 'csv') {
|
||||
const header = 'id,usuario_id,acao,entidade,entidade_id,created_at\n';
|
||||
const rows = data.map((r: any) => `${r.id},${r.usuario_id},${r.acao},${r.entidade},${r.entidade_id},${r.created_at}`).join('\n');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=audit_export.csv');
|
||||
return res.send(header + rows);
|
||||
}
|
||||
res.json(data);
|
||||
}
|
||||
}
|
||||
13
backend/src/modules/audit/audit.module.ts
Normal file
13
backend/src/modules/audit/audit.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuditLog } from '../dashboard/entities/audit-log.entity';
|
||||
import { AuditController } from './audit.controller';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AuditLog])],
|
||||
controllers: [AuditController],
|
||||
providers: [AuditService],
|
||||
exports: [AuditService],
|
||||
})
|
||||
export class AuditModule {}
|
||||
34
backend/src/modules/audit/audit.service.ts
Normal file
34
backend/src/modules/audit/audit.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AuditLog } from '../dashboard/entities/audit-log.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(
|
||||
@InjectRepository(AuditLog) private repo: Repository<AuditLog>,
|
||||
) {}
|
||||
|
||||
async logs(q: any) {
|
||||
const qb = this.repo.createQueryBuilder('a');
|
||||
if (q.entidade) qb.andWhere('a.entidade = :e', { e: q.entidade });
|
||||
if (q.acao) qb.andWhere('a.acao = :a', { a: q.acao });
|
||||
if (q.usuario_id) qb.andWhere('a.usuario_id = :u', { u: q.usuario_id });
|
||||
if (q.de) qb.andWhere('a.created_at >= :de', { de: q.de });
|
||||
if (q.ate) qb.andWhere('a.created_at <= :ate', { ate: q.ate });
|
||||
return qb.orderBy('a.created_at', 'DESC').limit(q.limit || 500).getMany();
|
||||
}
|
||||
|
||||
async complianceReport(q: any) {
|
||||
const qb = this.repo.createQueryBuilder('a')
|
||||
.select('a.entidade', 'entidade')
|
||||
.addSelect('a.acao', 'acao')
|
||||
.addSelect('COUNT(*)', 'total')
|
||||
.groupBy('a.entidade')
|
||||
.addGroupBy('a.acao')
|
||||
.orderBy('total', 'DESC');
|
||||
if (q.de) qb.andWhere('a.created_at >= :de', { de: q.de });
|
||||
if (q.ate) qb.andWhere('a.created_at <= :ate', { ate: q.ate });
|
||||
return qb.getRawMany();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,18 @@ export class Categoria {
|
||||
@Column({ nullable: true })
|
||||
categoria_pai_id: string;
|
||||
|
||||
@Column({ length: 10, nullable: true })
|
||||
tipo_investimento: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
tipo_manutencao: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
impacto_ambiental_esperado: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
potencial_geracao_residuos: string;
|
||||
|
||||
@Column({ default: true })
|
||||
ativo: boolean;
|
||||
|
||||
|
||||
@@ -7,7 +7,15 @@ export class DashboardController {
|
||||
|
||||
@Get('indicadores') indicadores() { return this.svc.indicadores(); }
|
||||
@Get('demandas-por-status') demandasPorStatus() { return this.svc.demandasPorStatus(); }
|
||||
@Get('consumo-orcamento') consumoOrcamento(@Query('ano') ano: string) { return this.svc.consumoOrcamento(parseInt(ano) || 2026); }
|
||||
@Get('demandas-detalhe') demandasDetalhe(@Query('status') status: string) { return this.svc.demandasDetalhe(status); }
|
||||
@Get('consumo-orcamento') consumoOrcamento(
|
||||
@Query('ano') ano: string,
|
||||
@Query('centro_custo_id') centroCustoId?: string,
|
||||
@Query('categoria_id') categoriaId?: string,
|
||||
) { return this.svc.consumoOrcamento(parseInt(ano) || 2026, centroCustoId, categoriaId); }
|
||||
@Get('categorias-quantidade') categoriasQuantidade() { return this.svc.categoriasQuantidade(); }
|
||||
@Get('busca') busca(@Query('q') q: string) { return this.svc.busca(q); }
|
||||
@Get('alertas') alertas(@Query('usuario_id') uid: string) { return this.svc.alertas(uid); }
|
||||
@Patch('alertas/:id/ler') marcarLido(@Param('id') id: string) { return this.svc.marcarAlertaLido(id); }
|
||||
@Get('esg') esgIndicadores() { return this.svc.esgIndicadores(); }
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import { OrcamentoPlanejado } from '../orcamento/entities/orcamento-planejado.en
|
||||
import { WorkflowAprovacao } from '../workflow/entities/workflow-aprovacao.entity';
|
||||
import { Alerta } from './entities/alerta.entity';
|
||||
import { AuditLog } from './entities/audit-log.entity';
|
||||
import { Fornecedor } from '../fornecedores/entities/fornecedor.entity';
|
||||
import { Categoria } from '../categorias/entities/categoria.entity';
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Demanda, Proposta, OrdemServico, OrcamentoPlanejado, WorkflowAprovacao, Alerta, AuditLog])],
|
||||
imports: [TypeOrmModule.forFeature([Demanda, Proposta, OrdemServico, OrcamentoPlanejado, WorkflowAprovacao, Alerta, AuditLog, Fornecedor, Categoria])],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService],
|
||||
exports: [DashboardService],
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Demanda } from '../demandas/entities/demanda.entity';
|
||||
import { Proposta } from '../propostas/entities/proposta.entity';
|
||||
import { OrdemServico } from '../ordens-servico/entities/ordem-servico.entity';
|
||||
import { OrcamentoPlanejado } from '../orcamento/entities/orcamento-planejado.entity';
|
||||
import { WorkflowAprovacao } from '../workflow/entities/workflow-aprovacao.entity';
|
||||
import { Alerta } from './entities/alerta.entity';
|
||||
import { Fornecedor } from '../fornecedores/entities/fornecedor.entity';
|
||||
import { Categoria } from '../categorias/entities/categoria.entity';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
@@ -17,6 +19,9 @@ export class DashboardService {
|
||||
@InjectRepository(OrcamentoPlanejado) private orcRepo: Repository<OrcamentoPlanejado>,
|
||||
@InjectRepository(WorkflowAprovacao) private wfRepo: Repository<WorkflowAprovacao>,
|
||||
@InjectRepository(Alerta) private alertaRepo: Repository<Alerta>,
|
||||
@InjectRepository(Fornecedor) private fornecedorRepo: Repository<Fornecedor>,
|
||||
@InjectRepository(Categoria) private categoriaRepo: Repository<Categoria>,
|
||||
private ds: DataSource,
|
||||
) {}
|
||||
|
||||
async indicadores() {
|
||||
@@ -44,8 +49,11 @@ export class DashboardService {
|
||||
return Object.entries(statusCount).map(([name, value]) => ({ name, value }));
|
||||
}
|
||||
|
||||
async consumoOrcamento(ano: number) {
|
||||
const items = await this.orcRepo.find({ where: { ano } });
|
||||
async consumoOrcamento(ano: number, centroCustoId?: string, categoriaId?: string) {
|
||||
const where: any = { ano };
|
||||
if (centroCustoId) where.centro_custo_id = centroCustoId;
|
||||
if (categoriaId) where.categoria_id = categoriaId;
|
||||
const items = await this.orcRepo.find({ where });
|
||||
const meses = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
const byMonth: Record<number, any> = {};
|
||||
for (const item of items) {
|
||||
@@ -61,6 +69,82 @@ export class DashboardService {
|
||||
});
|
||||
}
|
||||
|
||||
async demandasDetalhe(status: string) {
|
||||
const demandas = await this.demandaRepo.find();
|
||||
const categorias = await this.categoriaRepo.find();
|
||||
const catMap: Record<string, string> = {};
|
||||
categorias.forEach(c => { catMap[c.id] = c.nome; });
|
||||
|
||||
let filtered: Demanda[];
|
||||
switch (status) {
|
||||
case 'abertas':
|
||||
filtered = demandas.filter(d => ['aberta', 'em_escopo'].includes(d.status));
|
||||
break;
|
||||
case 'em_cotacao':
|
||||
filtered = demandas.filter(d => d.status === 'em_cotacao');
|
||||
break;
|
||||
case 'em_aprovacao':
|
||||
filtered = demandas.filter(d => ['propostas_recebidas', 'em_comparacao'].includes(d.status));
|
||||
break;
|
||||
case 'concluidas':
|
||||
filtered = demandas.filter(d => d.status === 'concluida');
|
||||
break;
|
||||
default:
|
||||
filtered = demandas.filter(d => d.status === status);
|
||||
}
|
||||
|
||||
return filtered.map(d => ({
|
||||
id: d.id,
|
||||
numero: d.numero,
|
||||
titulo: d.titulo,
|
||||
categoria: catMap[d.categoria_id] || '-',
|
||||
status: d.status,
|
||||
data: d.created_at,
|
||||
valor_estimado: d.valor_estimado,
|
||||
}));
|
||||
}
|
||||
|
||||
async categoriasQuantidade() {
|
||||
const demandas = await this.demandaRepo.find();
|
||||
const categorias = await this.categoriaRepo.find();
|
||||
const catMap: Record<string, string> = {};
|
||||
categorias.forEach(c => { catMap[c.id] = c.nome; });
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
for (const d of demandas) {
|
||||
const catName = catMap[d.categoria_id] || 'Outros';
|
||||
counts[catName] = (counts[catName] || 0) + 1;
|
||||
}
|
||||
|
||||
const colors = ['#E65100', '#1A237E', '#FF8F00', '#757575', '#4CAF50', '#9C27B0', '#00BCD4', '#F44336'];
|
||||
return Object.entries(counts).map(([name, value], i) => ({
|
||||
name,
|
||||
value,
|
||||
color: colors[i % colors.length],
|
||||
}));
|
||||
}
|
||||
|
||||
async busca(q: string) {
|
||||
if (!q || q.trim().length < 2) return { demandas: [], orcamentos: [] };
|
||||
const term = q.toLowerCase();
|
||||
|
||||
const demandas = await this.demandaRepo.find();
|
||||
const categorias = await this.categoriaRepo.find();
|
||||
const catMap: Record<string, string> = {};
|
||||
categorias.forEach(c => { catMap[c.id] = c.nome; });
|
||||
|
||||
const matchedDemandas = demandas.filter(d =>
|
||||
d.titulo?.toLowerCase().includes(term) ||
|
||||
(catMap[d.categoria_id] || '').toLowerCase().includes(term) ||
|
||||
String(d.numero).includes(term)
|
||||
).slice(0, 10).map(d => ({
|
||||
id: d.id, numero: d.numero, titulo: d.titulo,
|
||||
categoria: catMap[d.categoria_id] || '-', status: d.status,
|
||||
}));
|
||||
|
||||
return { demandas: matchedDemandas };
|
||||
}
|
||||
|
||||
async alertas(usuarioId?: string) {
|
||||
const where: any = { lido: false };
|
||||
if (usuarioId) where.usuario_id = usuarioId;
|
||||
@@ -70,4 +154,63 @@ export class DashboardService {
|
||||
async marcarAlertaLido(id: string) {
|
||||
await this.alertaRepo.update(id, { lido: true });
|
||||
}
|
||||
|
||||
async esgIndicadores() {
|
||||
// Categorias with tipo_manutencao
|
||||
const categorias = await this.categoriaRepo.find();
|
||||
const catMap: Record<string, any> = {};
|
||||
categorias.forEach(c => { catMap[c.id] = c; });
|
||||
|
||||
const demandas = await this.demandaRepo.find();
|
||||
const fornecedores = await this.fornecedorRepo.find({ where: { ativo: true } });
|
||||
|
||||
// Classify demandas by tipo_manutencao from their category
|
||||
let preventivas = 0, corretivas = 0, emergenciais = 0, totalClassificada = 0;
|
||||
const demandasAltoImpacto: any[] = [];
|
||||
|
||||
for (const d of demandas) {
|
||||
const cat = catMap[d.categoria_id];
|
||||
if (cat?.tipo_manutencao) {
|
||||
totalClassificada++;
|
||||
if (cat.tipo_manutencao === 'Preventiva') preventivas++;
|
||||
else if (cat.tipo_manutencao === 'Corretiva') corretivas++;
|
||||
else if (cat.tipo_manutencao === 'Emergencial') emergenciais++;
|
||||
}
|
||||
if (d.impacto_ambiental_demanda === 'Alto' || (cat?.impacto_ambiental_esperado === 'Alto' && !d.impacto_ambiental_demanda)) {
|
||||
demandasAltoImpacto.push({ id: d.id, numero: d.numero, titulo: d.titulo, status: d.status });
|
||||
}
|
||||
}
|
||||
|
||||
// Fornecedores ESG
|
||||
const esgInterm = fornecedores.filter(f => f.classificacao_esg === 'Intermediário' || f.classificacao_esg === 'Avançado').length;
|
||||
const esgBasico = fornecedores.filter(f => f.classificacao_esg === 'Básico').length;
|
||||
|
||||
// Evolução mensal preventiva (by created_at month)
|
||||
const evolucaoPreventiva: Record<number, number> = {};
|
||||
for (const d of demandas) {
|
||||
const cat = catMap[d.categoria_id];
|
||||
if (cat?.tipo_manutencao === 'Preventiva' && d.created_at) {
|
||||
const mes = new Date(d.created_at).getMonth() + 1;
|
||||
evolucaoPreventiva[mes] = (evolucaoPreventiva[mes] || 0) + 1;
|
||||
}
|
||||
}
|
||||
const meses = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
|
||||
const evolucao = meses.map((name, i) => ({ name, preventivas: evolucaoPreventiva[i + 1] || 0 }));
|
||||
|
||||
return {
|
||||
pct_preventivas: totalClassificada > 0 ? Math.round(preventivas / totalClassificada * 100) : 0,
|
||||
pct_corretivas: totalClassificada > 0 ? Math.round(corretivas / totalClassificada * 100) : 0,
|
||||
pct_emergenciais: totalClassificada > 0 ? Math.round(emergenciais / totalClassificada * 100) : 0,
|
||||
total_preventivas: preventivas,
|
||||
total_corretivas: corretivas,
|
||||
total_emergenciais: emergenciais,
|
||||
pct_fornecedores_esg_bom: fornecedores.length > 0 ? Math.round(esgInterm / fornecedores.length * 100) : 0,
|
||||
fornecedores_esg_basico: esgBasico,
|
||||
fornecedores_esg_intermediario_avancado: esgInterm,
|
||||
total_fornecedores: fornecedores.length,
|
||||
demandas_alto_impacto: demandasAltoImpacto,
|
||||
demandas_alto_impacto_count: demandasAltoImpacto.length,
|
||||
evolucao_preventiva: evolucao,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export class DemandasController {
|
||||
@Get(':id') findOne(@Param('id') id: string) { return this.svc.findOne(id); }
|
||||
@Post() create(@Body() body: any) { return this.svc.create(body); }
|
||||
@Patch(':id') update(@Param('id') id: string, @Body() body: any) { return this.svc.update(id, body); }
|
||||
@Delete(':id') remove(@Param('id') id: string) { return this.svc.remove(id); }
|
||||
|
||||
@Post(':id/publicar') publicar(@Param('id') id: string) { return this.svc.updateStatus(id, 'aberta'); }
|
||||
@Post(':id/cancelar') cancelar(@Param('id') id: string) { return this.svc.updateStatus(id, 'cancelada'); }
|
||||
|
||||
@@ -22,6 +22,7 @@ export class DemandasService {
|
||||
findOne(id: string) { return this.repo.findOne({ where: { id }, relations: ['itens_linha'] }); }
|
||||
create(data: Partial<Demanda>) { return this.repo.save(data); }
|
||||
async update(id: string, data: Partial<Demanda>) { await this.repo.update(id, data); return this.findOne(id); }
|
||||
async remove(id: string) { await this.repo.delete(id); return { deleted: true }; }
|
||||
|
||||
async updateStatus(id: string, status: string) {
|
||||
await this.repo.update(id, { status });
|
||||
|
||||
@@ -24,6 +24,9 @@ export class Demanda {
|
||||
@Column()
|
||||
categoria_id: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
subcategoria_id: string;
|
||||
|
||||
@Column({ length: 20 })
|
||||
criticidade: string;
|
||||
|
||||
@@ -39,6 +42,15 @@ export class Demanda {
|
||||
@Column({ nullable: true })
|
||||
gestor_id: string;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor_estimado: number;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
impacto_ambiental_demanda: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
justificativa_manutencao_emergencial: string;
|
||||
|
||||
@Column({ type: 'simple-json', default: '[]' })
|
||||
documentos: any;
|
||||
|
||||
|
||||
36
backend/src/modules/documentos/documentos.controller.ts
Normal file
36
backend/src/modules/documentos/documentos.controller.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Controller, Get, Post, Delete, Param, Res, UploadedFile, UseInterceptors, Body, Query } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import type { Response } from 'express';
|
||||
import { DocumentosService } from './documentos.service';
|
||||
|
||||
@Controller()
|
||||
export class DocumentosController {
|
||||
constructor(private svc: DocumentosService) {}
|
||||
|
||||
@Get('demandas/:id/documentos')
|
||||
findByDemanda(@Param('id') demandaId: string) {
|
||||
return this.svc.findByDemanda(demandaId);
|
||||
}
|
||||
|
||||
@Post('demandas/:id/documentos')
|
||||
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 50 * 1024 * 1024 } }))
|
||||
upload(
|
||||
@Param('id') demandaId: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body('tipo') tipo?: string,
|
||||
) {
|
||||
return this.svc.upload(demandaId, file, tipo);
|
||||
}
|
||||
|
||||
@Get('documentos/:id/download')
|
||||
async download(@Param('id') id: string, @Res() res: Response) {
|
||||
const doc = await this.svc.findOne(id);
|
||||
const filePath = this.svc.getFilePath(doc);
|
||||
res.download(filePath, doc.nome_arquivo);
|
||||
}
|
||||
|
||||
@Delete('documentos/:id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.svc.remove(id);
|
||||
}
|
||||
}
|
||||
13
backend/src/modules/documentos/documentos.module.ts
Normal file
13
backend/src/modules/documentos/documentos.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Documento } from './entities/documento.entity';
|
||||
import { DocumentosController } from './documentos.controller';
|
||||
import { DocumentosService } from './documentos.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Documento])],
|
||||
controllers: [DocumentosController],
|
||||
providers: [DocumentosService],
|
||||
exports: [DocumentosService],
|
||||
})
|
||||
export class DocumentosModule {}
|
||||
58
backend/src/modules/documentos/documentos.service.ts
Normal file
58
backend/src/modules/documentos/documentos.service.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Documento } from './entities/documento.entity';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/opt/hefesto/uploads';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentosService {
|
||||
constructor(@InjectRepository(Documento) private repo: Repository<Documento>) {
|
||||
// Ensure upload dir exists
|
||||
if (!fs.existsSync(UPLOAD_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
findByDemanda(demandaId: string) {
|
||||
return this.repo.find({ where: { demanda_id: demandaId }, order: { created_at: 'DESC' } });
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const doc = await this.repo.findOne({ where: { id } });
|
||||
if (!doc) throw new NotFoundException('Documento não encontrado');
|
||||
return doc;
|
||||
}
|
||||
|
||||
async upload(demandaId: string, file: Express.Multer.File, tipo?: string) {
|
||||
const ext = path.extname(file.originalname);
|
||||
const filename = `${demandaId}_${Date.now()}${ext}`;
|
||||
const destPath = path.join(UPLOAD_DIR, filename);
|
||||
|
||||
fs.writeFileSync(destPath, file.buffer);
|
||||
|
||||
return this.repo.save({
|
||||
demanda_id: demandaId,
|
||||
nome_arquivo: file.originalname,
|
||||
tipo: tipo || 'outro',
|
||||
caminho: filename,
|
||||
tamanho: file.size,
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const doc = await this.findOne(id);
|
||||
const filePath = path.join(UPLOAD_DIR, doc.caminho);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
await this.repo.delete(id);
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
getFilePath(doc: Documento) {
|
||||
return path.join(UPLOAD_DIR, doc.caminho);
|
||||
}
|
||||
}
|
||||
25
backend/src/modules/documentos/entities/documento.entity.ts
Normal file
25
backend/src/modules/documentos/entities/documento.entity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('documentos')
|
||||
export class Documento {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
demanda_id: string;
|
||||
|
||||
@Column({ length: 500 })
|
||||
nome_arquivo: string;
|
||||
|
||||
@Column({ length: 50, default: 'outro' })
|
||||
tipo: string; // planta | foto | laudo | outro
|
||||
|
||||
@Column({ length: 1000 })
|
||||
caminho: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
tamanho: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
22
backend/src/modules/esg/entities/esg-meta.entity.ts
Normal file
22
backend/src/modules/esg/entities/esg-meta.entity.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('esg_metas')
|
||||
export class EsgMeta {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 20 })
|
||||
tipo: string;
|
||||
|
||||
@Column({ type: 'float' })
|
||||
meta_valor: number;
|
||||
|
||||
@Column()
|
||||
periodo_ano: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
local_id: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
40
backend/src/modules/esg/entities/esg-metrica.entity.ts
Normal file
40
backend/src/modules/esg/entities/esg-metrica.entity.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('esg_metricas')
|
||||
export class EsgMetrica {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 20 })
|
||||
tipo: string; // energia | agua | residuos | emissoes
|
||||
|
||||
@Column({ length: 30 })
|
||||
unidade_medida: string;
|
||||
|
||||
@Column({ type: 'float' })
|
||||
valor: number;
|
||||
|
||||
@Column()
|
||||
periodo_mes: number;
|
||||
|
||||
@Column()
|
||||
periodo_ano: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
local_id: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
centro_custo_id: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
fonte_dados: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
observacoes: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
28
backend/src/modules/esg/esg.controller.ts
Normal file
28
backend/src/modules/esg/esg.controller.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query } from '@nestjs/common';
|
||||
import { EsgService } from './esg.service';
|
||||
|
||||
@Controller('esg')
|
||||
export class EsgController {
|
||||
constructor(private readonly svc: EsgService) {}
|
||||
|
||||
@Get('metricas')
|
||||
findMetricas(@Query() q: any) { return this.svc.findMetricas(q); }
|
||||
|
||||
@Post('metricas')
|
||||
createMetrica(@Body() dto: any) { return this.svc.createMetrica(dto); }
|
||||
|
||||
@Patch('metricas/:id')
|
||||
updateMetrica(@Param('id') id: string, @Body() dto: any) { return this.svc.updateMetrica(id, dto); }
|
||||
|
||||
@Get('metricas/resumo')
|
||||
resumo(@Query() q: any) { return this.svc.resumo(q); }
|
||||
|
||||
@Get('metas')
|
||||
findMetas(@Query() q: any) { return this.svc.findMetas(q); }
|
||||
|
||||
@Post('metas')
|
||||
createMeta(@Body() dto: any) { return this.svc.createMeta(dto); }
|
||||
|
||||
@Get('dashboard')
|
||||
dashboard(@Query() q: any) { return this.svc.dashboard(q); }
|
||||
}
|
||||
14
backend/src/modules/esg/esg.module.ts
Normal file
14
backend/src/modules/esg/esg.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EsgMetrica } from './entities/esg-metrica.entity';
|
||||
import { EsgMeta } from './entities/esg-meta.entity';
|
||||
import { EsgController } from './esg.controller';
|
||||
import { EsgService } from './esg.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([EsgMetrica, EsgMeta])],
|
||||
controllers: [EsgController],
|
||||
providers: [EsgService],
|
||||
exports: [EsgService],
|
||||
})
|
||||
export class EsgModule {}
|
||||
75
backend/src/modules/esg/esg.service.ts
Normal file
75
backend/src/modules/esg/esg.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { EsgMetrica } from './entities/esg-metrica.entity';
|
||||
import { EsgMeta } from './entities/esg-meta.entity';
|
||||
|
||||
@Injectable()
|
||||
export class EsgService {
|
||||
constructor(
|
||||
@InjectRepository(EsgMetrica) private metricaRepo: Repository<EsgMetrica>,
|
||||
@InjectRepository(EsgMeta) private metaRepo: Repository<EsgMeta>,
|
||||
) {}
|
||||
|
||||
async findMetricas(q: any) {
|
||||
const qb = this.metricaRepo.createQueryBuilder('m');
|
||||
if (q.tipo) qb.andWhere('m.tipo = :tipo', { tipo: q.tipo });
|
||||
if (q.ano) qb.andWhere('m.periodo_ano = :ano', { ano: q.ano });
|
||||
if (q.mes) qb.andWhere('m.periodo_mes = :mes', { mes: q.mes });
|
||||
if (q.local_id) qb.andWhere('m.local_id = :local_id', { local_id: q.local_id });
|
||||
return qb.orderBy('m.periodo_ano', 'DESC').addOrderBy('m.periodo_mes', 'DESC').getMany();
|
||||
}
|
||||
|
||||
async createMetrica(dto: any) {
|
||||
return this.metricaRepo.save(this.metricaRepo.create(dto));
|
||||
}
|
||||
|
||||
async updateMetrica(id: string, dto: any) {
|
||||
await this.metricaRepo.update(id, dto);
|
||||
return this.metricaRepo.findOneBy({ id });
|
||||
}
|
||||
|
||||
async resumo(q: any) {
|
||||
const ano = q.ano || new Date().getFullYear();
|
||||
const rows = await this.metricaRepo.createQueryBuilder('m')
|
||||
.select('m.tipo', 'tipo')
|
||||
.addSelect('m.unidade_medida', 'unidade')
|
||||
.addSelect('SUM(m.valor)', 'total')
|
||||
.addSelect('AVG(m.valor)', 'media')
|
||||
.addSelect('COUNT(*)', 'registros')
|
||||
.where('m.periodo_ano = :ano', { ano })
|
||||
.groupBy('m.tipo')
|
||||
.addGroupBy('m.unidade_medida')
|
||||
.getRawMany();
|
||||
return rows;
|
||||
}
|
||||
|
||||
async findMetas(q: any) {
|
||||
const qb = this.metaRepo.createQueryBuilder('m');
|
||||
if (q.ano) qb.andWhere('m.periodo_ano = :ano', { ano: q.ano });
|
||||
if (q.tipo) qb.andWhere('m.tipo = :tipo', { tipo: q.tipo });
|
||||
return qb.getMany();
|
||||
}
|
||||
|
||||
async createMeta(dto: any) {
|
||||
return this.metaRepo.save(this.metaRepo.create(dto));
|
||||
}
|
||||
|
||||
async dashboard(q: any) {
|
||||
const ano = q.ano || new Date().getFullYear();
|
||||
const metricas = await this.metricaRepo.createQueryBuilder('m')
|
||||
.select('m.tipo', 'tipo')
|
||||
.addSelect('m.periodo_mes', 'mes')
|
||||
.addSelect('SUM(m.valor)', 'total')
|
||||
.where('m.periodo_ano = :ano', { ano })
|
||||
.groupBy('m.tipo')
|
||||
.addGroupBy('m.periodo_mes')
|
||||
.orderBy('m.periodo_mes', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
const metas = await this.metaRepo.find({ where: { periodo_ano: ano } });
|
||||
const resumo = await this.resumo({ ano });
|
||||
|
||||
return { ano, metricas_por_mes: metricas, metas, resumo };
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,24 @@ export class Fornecedor {
|
||||
@Column({ nullable: true })
|
||||
usuario_id: string;
|
||||
|
||||
@Column({ length: 200, nullable: true })
|
||||
nome_contato: string;
|
||||
|
||||
@Column({ default: false })
|
||||
possui_politica_ambiental: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
possui_politica_sst: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
declara_uso_epi: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
equipe_treinada: boolean;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
classificacao_esg: string;
|
||||
|
||||
@Column({ default: true })
|
||||
ativo: boolean;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Fornecedor } from './entities/fornecedor.entity';
|
||||
@@ -15,7 +15,16 @@ export class FornecedoresService {
|
||||
findOne(id: string) { return this.repo.findOne({ where: { id }, relations: ['certidoes'] }); }
|
||||
create(data: Partial<Fornecedor>) { return this.repo.save(data); }
|
||||
async update(id: string, data: Partial<Fornecedor>) { await this.repo.update(id, data); return this.findOne(id); }
|
||||
async remove(id: string) { await this.repo.update(id, { ativo: false }); }
|
||||
|
||||
async remove(id: string) {
|
||||
// Check for associated OS via direct query
|
||||
const result = await this.repo.query('SELECT COUNT(*) as count FROM ordens_servico WHERE fornecedor_id = $1', [id]);
|
||||
const count = parseInt(result[0]?.count || '0');
|
||||
if (count > 0) {
|
||||
throw new BadRequestException('Não é possível excluir este fornecedor pois existem Ordens de Serviço associadas.');
|
||||
}
|
||||
await this.repo.update(id, { ativo: false });
|
||||
}
|
||||
|
||||
findCertidoes(fornecedorId: string) { return this.certRepo.find({ where: { fornecedor_id: fornecedorId } }); }
|
||||
createCertidao(data: Partial<Certidao>) { return this.certRepo.save(data); }
|
||||
|
||||
14
backend/src/modules/import/import.controller.ts
Normal file
14
backend/src/modules/import/import.controller.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Controller, Post, UploadedFile, UseInterceptors, Query } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ImportService } from './import.service';
|
||||
|
||||
@Controller('import')
|
||||
export class ImportController {
|
||||
constructor(private readonly svc: ImportService) {}
|
||||
|
||||
@Post('excel')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async importExcel(@UploadedFile() file: any, @Query('tipo') tipo: string) {
|
||||
return this.svc.importExcel(file, tipo);
|
||||
}
|
||||
}
|
||||
9
backend/src/modules/import/import.module.ts
Normal file
9
backend/src/modules/import/import.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ImportController } from './import.controller';
|
||||
import { ImportService } from './import.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ImportController],
|
||||
providers: [ImportService],
|
||||
})
|
||||
export class ImportModule {}
|
||||
60
backend/src/modules/import/import.service.ts
Normal file
60
backend/src/modules/import/import.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
@Injectable()
|
||||
export class ImportService {
|
||||
constructor(private ds: DataSource) {}
|
||||
|
||||
async importExcel(file: any, tipo: string) {
|
||||
if (!file) throw new BadRequestException('Nenhum arquivo enviado');
|
||||
if (!tipo || !['orcamento', 'demandas'].includes(tipo)) {
|
||||
throw new BadRequestException('Tipo deve ser "orcamento" ou "demandas"');
|
||||
}
|
||||
|
||||
const wb = XLSX.read(file.buffer, { type: 'buffer' });
|
||||
const sheet = wb.Sheets[wb.SheetNames[0]];
|
||||
const rows: any[] = XLSX.utils.sheet_to_json(sheet);
|
||||
|
||||
if (!rows.length) throw new BadRequestException('Planilha vazia');
|
||||
|
||||
const errors: string[] = [];
|
||||
const valid: any[] = [];
|
||||
|
||||
if (tipo === 'orcamento') {
|
||||
const required = ['ano', 'mes', 'centro_custo_id', 'categoria_id', 'valor_planejado'];
|
||||
rows.forEach((r, i) => {
|
||||
const missing = required.filter(f => r[f] === undefined || r[f] === '');
|
||||
if (missing.length) { errors.push(`Linha ${i + 2}: campos faltando: ${missing.join(', ')}`); }
|
||||
else { valid.push(r); }
|
||||
});
|
||||
} else {
|
||||
const required = ['titulo', 'local_id', 'centro_custo_id', 'categoria_id', 'criticidade', 'solicitante_id'];
|
||||
rows.forEach((r, i) => {
|
||||
const missing = required.filter(f => r[f] === undefined || r[f] === '');
|
||||
if (missing.length) { errors.push(`Linha ${i + 2}: campos faltando: ${missing.join(', ')}`); }
|
||||
else { valid.push({ ...r, status: r.status || 'rascunho' }); }
|
||||
});
|
||||
}
|
||||
|
||||
if (errors.length && !valid.length) {
|
||||
return { sucesso: false, erros: errors, importados: 0 };
|
||||
}
|
||||
|
||||
const table = tipo === 'orcamento' ? 'orcamento_planejado' : 'demandas';
|
||||
let importados = 0;
|
||||
for (const row of valid) {
|
||||
try {
|
||||
const cols = Object.keys(row);
|
||||
const vals = cols.map(c => row[c]);
|
||||
const placeholders = cols.map(() => '?').join(',');
|
||||
await this.ds.query(`INSERT INTO ${table} (${cols.join(',')}) VALUES (${placeholders})`, vals);
|
||||
importados++;
|
||||
} catch (e: any) {
|
||||
errors.push(`Erro ao inserir: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { sucesso: true, importados, erros: errors, total_linhas: rows.length };
|
||||
}
|
||||
}
|
||||
43
backend/src/modules/kpis/entities/kpi.entity.ts
Normal file
43
backend/src/modules/kpis/entities/kpi.entity.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('kpis')
|
||||
export class Kpi {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 200 })
|
||||
nome: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
descricao: string;
|
||||
|
||||
@Column({ length: 300, nullable: true })
|
||||
formula: string;
|
||||
|
||||
@Column({ length: 30 })
|
||||
categoria: string; // financeiro | operacional | qualidade | prazo
|
||||
|
||||
@Column({ length: 30, nullable: true })
|
||||
unidade: string;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
meta_valor: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor_atual: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
periodo_mes: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
periodo_ano: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
centro_custo_id: string;
|
||||
|
||||
@Column({ length: 20, default: 'verde' })
|
||||
status: string; // verde | amarelo | vermelho
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
16
backend/src/modules/kpis/kpis.controller.ts
Normal file
16
backend/src/modules/kpis/kpis.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
|
||||
import { KpisService } from './kpis.service';
|
||||
|
||||
@Controller('kpis')
|
||||
export class KpisController {
|
||||
constructor(private readonly svc: KpisService) {}
|
||||
|
||||
@Get()
|
||||
findAll(@Query() q: any) { return this.svc.findAll(q); }
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: any) { return this.svc.create(dto); }
|
||||
|
||||
@Get('dashboard')
|
||||
dashboard(@Query() q: any) { return this.svc.dashboard(q); }
|
||||
}
|
||||
13
backend/src/modules/kpis/kpis.module.ts
Normal file
13
backend/src/modules/kpis/kpis.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Kpi } from './entities/kpi.entity';
|
||||
import { KpisController } from './kpis.controller';
|
||||
import { KpisService } from './kpis.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Kpi])],
|
||||
controllers: [KpisController],
|
||||
providers: [KpisService],
|
||||
exports: [KpisService],
|
||||
})
|
||||
export class KpisModule {}
|
||||
75
backend/src/modules/kpis/kpis.service.ts
Normal file
75
backend/src/modules/kpis/kpis.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Kpi } from './entities/kpi.entity';
|
||||
|
||||
@Injectable()
|
||||
export class KpisService {
|
||||
constructor(
|
||||
@InjectRepository(Kpi) private kpiRepo: Repository<Kpi>,
|
||||
private ds: DataSource,
|
||||
) {}
|
||||
|
||||
async findAll(q: any) {
|
||||
const qb = this.kpiRepo.createQueryBuilder('k');
|
||||
if (q.categoria) qb.andWhere('k.categoria = :cat', { cat: q.categoria });
|
||||
if (q.ano) qb.andWhere('k.periodo_ano = :ano', { ano: q.ano });
|
||||
if (q.centro_custo_id) qb.andWhere('k.centro_custo_id = :cc', { cc: q.centro_custo_id });
|
||||
return qb.orderBy('k.categoria', 'ASC').getMany();
|
||||
}
|
||||
|
||||
async create(dto: any) {
|
||||
return this.kpiRepo.save(this.kpiRepo.create(dto));
|
||||
}
|
||||
|
||||
async dashboard(q: any) {
|
||||
const ano = q.ano || new Date().getFullYear();
|
||||
const mes = q.mes || new Date().getMonth() + 1;
|
||||
|
||||
// Auto-calculated KPIs
|
||||
const calculated: any[] = [];
|
||||
|
||||
// % budget consumed
|
||||
try {
|
||||
const [budget] = await this.ds.query(`
|
||||
SELECT COALESCE(SUM(valor_planejado),0) as planejado, COALESCE(SUM(valor_realizado),0) as realizado
|
||||
FROM orcamento_planejado WHERE ano = ? AND mes = ?`, [ano, mes]);
|
||||
if (budget && budget.planejado > 0) {
|
||||
const pct = (budget.realizado / budget.planejado) * 100;
|
||||
calculated.push({ nome: '% Orçamento Consumido', categoria: 'financeiro', unidade: '%', valor_atual: Math.round(pct * 100) / 100, meta_valor: 100, status: pct > 90 ? 'vermelho' : pct > 70 ? 'amarelo' : 'verde' });
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Avg time to close OS
|
||||
try {
|
||||
const [osTime] = await this.ds.query(`
|
||||
SELECT AVG(julianday(data_conclusao) - julianday(data_inicio)) as avg_dias
|
||||
FROM ordens_servico WHERE status = 'concluida' AND data_inicio IS NOT NULL AND data_conclusao IS NOT NULL`);
|
||||
if (osTime && osTime.avg_dias) {
|
||||
calculated.push({ nome: 'Tempo Médio Fechamento OS', categoria: 'prazo', unidade: 'dias', valor_atual: Math.round(osTime.avg_dias * 10) / 10, meta_valor: 15, status: osTime.avg_dias > 20 ? 'vermelho' : osTime.avg_dias > 15 ? 'amarelo' : 'verde' });
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Supplier rating avg
|
||||
try {
|
||||
const [rating] = await this.ds.query(`SELECT AVG(nota) as avg_nota FROM avaliacoes`);
|
||||
if (rating && rating.avg_nota) {
|
||||
calculated.push({ nome: 'Nota Média Fornecedores', categoria: 'qualidade', unidade: 'nota', valor_atual: Math.round(rating.avg_nota * 10) / 10, meta_valor: 4.0, status: rating.avg_nota < 3 ? 'vermelho' : rating.avg_nota < 3.5 ? 'amarelo' : 'verde' });
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Demand completion rate
|
||||
try {
|
||||
const [demandas] = await this.ds.query(`
|
||||
SELECT COUNT(*) as total, SUM(CASE WHEN status IN ('concluida','finalizada') THEN 1 ELSE 0 END) as concluidas
|
||||
FROM demandas`);
|
||||
if (demandas && demandas.total > 0) {
|
||||
const rate = (demandas.concluidas / demandas.total) * 100;
|
||||
calculated.push({ nome: 'Taxa Conclusão Demandas', categoria: 'operacional', unidade: '%', valor_atual: Math.round(rate * 100) / 100, meta_valor: 80, status: rate < 50 ? 'vermelho' : rate < 70 ? 'amarelo' : 'verde' });
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
const stored = await this.kpiRepo.find({ where: { periodo_ano: ano } });
|
||||
return { ano, mes, kpis_calculados: calculated, kpis_cadastrados: stored };
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,15 @@ export class Local {
|
||||
@Column({ nullable: true })
|
||||
responsavel_id: string;
|
||||
|
||||
@Column({ length: 30, nullable: true })
|
||||
tipo_operacao_local: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
classificacao_impacto_ambiental: string;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
praticas_sustentaveis: string[];
|
||||
|
||||
@Column({ default: true })
|
||||
ativo: boolean;
|
||||
|
||||
|
||||
37
backend/src/modules/metas/entities/meta.entity.ts
Normal file
37
backend/src/modules/metas/entities/meta.entity.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('metas')
|
||||
export class Meta {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 300 })
|
||||
titulo: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
descricao: string;
|
||||
|
||||
@Column({ length: 30 })
|
||||
tipo: string; // orcamento | operacional | esg
|
||||
|
||||
@Column({ nullable: true })
|
||||
centro_custo_id: string;
|
||||
|
||||
@Column({ type: 'float' })
|
||||
valor_meta: number;
|
||||
|
||||
@Column({ type: 'float', default: 0 })
|
||||
valor_atual: number;
|
||||
|
||||
@Column({ length: 30, nullable: true })
|
||||
unidade: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
prazo: string;
|
||||
|
||||
@Column({ length: 30, default: 'em_andamento' })
|
||||
status: string; // em_andamento | atingida | atrasada
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
19
backend/src/modules/metas/metas.controller.ts
Normal file
19
backend/src/modules/metas/metas.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query } from '@nestjs/common';
|
||||
import { MetasService } from './metas.service';
|
||||
|
||||
@Controller('metas')
|
||||
export class MetasController {
|
||||
constructor(private readonly svc: MetasService) {}
|
||||
|
||||
@Get()
|
||||
findAll(@Query() q: any) { return this.svc.findAll(q); }
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: any) { return this.svc.create(dto); }
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() dto: any) { return this.svc.update(id, dto); }
|
||||
|
||||
@Get('progresso')
|
||||
progresso() { return this.svc.progresso(); }
|
||||
}
|
||||
13
backend/src/modules/metas/metas.module.ts
Normal file
13
backend/src/modules/metas/metas.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Meta } from './entities/meta.entity';
|
||||
import { MetasController } from './metas.controller';
|
||||
import { MetasService } from './metas.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Meta])],
|
||||
controllers: [MetasController],
|
||||
providers: [MetasService],
|
||||
exports: [MetasService],
|
||||
})
|
||||
export class MetasModule {}
|
||||
37
backend/src/modules/metas/metas.service.ts
Normal file
37
backend/src/modules/metas/metas.service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Meta } from './entities/meta.entity';
|
||||
|
||||
@Injectable()
|
||||
export class MetasService {
|
||||
constructor(@InjectRepository(Meta) private repo: Repository<Meta>) {}
|
||||
|
||||
async findAll(q: any) {
|
||||
const qb = this.repo.createQueryBuilder('m');
|
||||
if (q.tipo) qb.andWhere('m.tipo = :tipo', { tipo: q.tipo });
|
||||
if (q.status) qb.andWhere('m.status = :s', { s: q.status });
|
||||
if (q.centro_custo_id) qb.andWhere('m.centro_custo_id = :cc', { cc: q.centro_custo_id });
|
||||
return qb.orderBy('m.created_at', 'DESC').getMany();
|
||||
}
|
||||
|
||||
async create(dto: any) { return this.repo.save(this.repo.create(dto)); }
|
||||
|
||||
async update(id: string, dto: any) {
|
||||
await this.repo.update(id, dto);
|
||||
return this.repo.findOneBy({ id });
|
||||
}
|
||||
|
||||
async progresso() {
|
||||
const all = await this.repo.find();
|
||||
const por_tipo = {} as any;
|
||||
for (const m of all) {
|
||||
if (!por_tipo[m.tipo]) por_tipo[m.tipo] = { total: 0, atingidas: 0, em_andamento: 0, atrasadas: 0 };
|
||||
por_tipo[m.tipo].total++;
|
||||
por_tipo[m.tipo][m.status]++;
|
||||
}
|
||||
const total = all.length;
|
||||
const atingidas = all.filter(m => m.status === 'atingida').length;
|
||||
return { total, atingidas, percentual_conclusao: total ? Math.round((atingidas / total) * 100) : 0, por_tipo, metas: all };
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,12 @@ export class OrcamentoPlanejado {
|
||||
@Column({ type: 'float', default: 0 })
|
||||
valor_realizado: number;
|
||||
|
||||
@Column({ length: 10, default: 'mensal' })
|
||||
tipo_periodo: string;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor_anual: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Param, Query } from '@nestjs/common';
|
||||
import { OrcamentoService } from './orcamento.service';
|
||||
|
||||
@Controller('orcamento')
|
||||
@@ -7,7 +7,9 @@ export class OrcamentoController {
|
||||
|
||||
@Get() findAll(@Query() query: any) { return this.svc.findAll(query); }
|
||||
@Get('resumo') resumo(@Query('ano') ano: string) { return this.svc.resumo(parseInt(ano) || 2026); }
|
||||
@Get('resumo-investimento') resumoInvestimento(@Query('ano') ano: string) { return this.svc.resumoInvestimento(parseInt(ano) || 2026); }
|
||||
@Get(':id') findOne(@Param('id') id: string) { return this.svc.findOne(id); }
|
||||
@Post() create(@Body() body: any) { return this.svc.create(body); }
|
||||
@Patch(':id') update(@Param('id') id: string, @Body() body: any) { return this.svc.update(id, body); }
|
||||
@Delete(':id') remove(@Param('id') id: string) { return this.svc.remove(id); }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { OrcamentoPlanejado } from './entities/orcamento-planejado.entity';
|
||||
import { Categoria } from '../categorias/entities/categoria.entity';
|
||||
import { OrcamentoController } from './orcamento.controller';
|
||||
import { OrcamentoService } from './orcamento.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([OrcamentoPlanejado])],
|
||||
imports: [TypeOrmModule.forFeature([OrcamentoPlanejado, Categoria])],
|
||||
controllers: [OrcamentoController],
|
||||
providers: [OrcamentoService],
|
||||
exports: [OrcamentoService],
|
||||
|
||||
@@ -2,22 +2,37 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { OrcamentoPlanejado } from './entities/orcamento-planejado.entity';
|
||||
import { Categoria } from '../categorias/entities/categoria.entity';
|
||||
|
||||
@Injectable()
|
||||
export class OrcamentoService {
|
||||
constructor(@InjectRepository(OrcamentoPlanejado) private repo: Repository<OrcamentoPlanejado>) {}
|
||||
constructor(
|
||||
@InjectRepository(OrcamentoPlanejado) private repo: Repository<OrcamentoPlanejado>,
|
||||
@InjectRepository(Categoria) private categoriaRepo: Repository<Categoria>,
|
||||
) {}
|
||||
|
||||
findAll(query?: any) {
|
||||
async findAll(query?: any) {
|
||||
const where: any = {};
|
||||
if (query?.ano) where.ano = query.ano;
|
||||
if (query?.mes) where.mes = query.mes;
|
||||
if (query?.centro_custo_id) where.centro_custo_id = query.centro_custo_id;
|
||||
return this.repo.find({ where });
|
||||
const items = await this.repo.find({ where });
|
||||
|
||||
// Enrich with tipo_investimento from categoria
|
||||
const categorias = await this.categoriaRepo.find();
|
||||
const catMap: Record<string, string> = {};
|
||||
categorias.forEach(c => { catMap[c.id] = c.tipo_investimento || ''; });
|
||||
|
||||
return items.map(item => ({
|
||||
...item,
|
||||
tipo_investimento: catMap[item.categoria_id] || '',
|
||||
}));
|
||||
}
|
||||
|
||||
findOne(id: string) { return this.repo.findOne({ where: { id } }); }
|
||||
create(data: Partial<OrcamentoPlanejado>) { return this.repo.save(data); }
|
||||
async update(id: string, data: Partial<OrcamentoPlanejado>) { await this.repo.update(id, data); return this.findOne(id); }
|
||||
async remove(id: string) { await this.repo.delete(id); return { deleted: true }; }
|
||||
|
||||
async resumo(ano: number) {
|
||||
const items = await this.repo.find({ where: { ano } });
|
||||
@@ -30,4 +45,23 @@ export class OrcamentoService {
|
||||
}
|
||||
return Object.values(byMonth).sort((a: any, b: any) => a.mes - b.mes);
|
||||
}
|
||||
|
||||
async resumoInvestimento(ano: number) {
|
||||
const items = await this.repo.find({ where: { ano } });
|
||||
const categorias = await this.categoriaRepo.find();
|
||||
const catMap: Record<string, string> = {};
|
||||
categorias.forEach(c => { catMap[c.id] = c.tipo_investimento || 'Não definido'; });
|
||||
|
||||
const result: Record<string, { tipo: string; planejado: number; realizado: number; economia: number }> = {};
|
||||
for (const item of items) {
|
||||
const tipo = catMap[item.categoria_id] || 'Não definido';
|
||||
if (!result[tipo]) result[tipo] = { tipo, planejado: 0, realizado: 0, economia: 0 };
|
||||
result[tipo].planejado += item.valor_planejado;
|
||||
result[tipo].realizado += item.valor_realizado;
|
||||
}
|
||||
for (const r of Object.values(result)) {
|
||||
r.economia = r.planejado - r.realizado;
|
||||
}
|
||||
return Object.values(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('documento_versoes')
|
||||
export class DocumentoVersao {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
ordem_servico_id: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
fornecedor_id: string;
|
||||
|
||||
@Column({ length: 500 })
|
||||
nome_arquivo: string;
|
||||
|
||||
@Column({ length: 1000 })
|
||||
caminho: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
tamanho: number;
|
||||
|
||||
@Column({ type: 'integer', default: 1 })
|
||||
versao: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor_bruto: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor_liquido: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
iss: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
inss: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
pcc: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
condicao_pagamento: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
prazo_execucao: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
data_estimada_entrega: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
}
|
||||
@@ -8,27 +8,63 @@ export class OrdemServico {
|
||||
@Column({ type: 'integer', unique: true, nullable: true })
|
||||
numero: number;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
demanda_id: string;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
proposta_id: string;
|
||||
|
||||
@Column()
|
||||
@Column({ nullable: true })
|
||||
fornecedor_id: string;
|
||||
|
||||
@Column({ type: 'float' })
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor: number;
|
||||
|
||||
@Column({ length: 30, default: 'emitida' })
|
||||
status: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
data: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
data_inicio: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
data_conclusao: string;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor_bruto: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
valor_liquido: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
iss: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
inss: number;
|
||||
|
||||
@Column({ type: 'float', nullable: true })
|
||||
pcc: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
condicao_pagamento: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
prazo_execucao: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
data_estimada_entrega: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
uso_material_sustentavel: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
gera_residuos: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
descarte_certificado: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
observacoes: string;
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Param, Res, UploadedFile, UseInterceptors, Query } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import type { Response } from 'express';
|
||||
import { OrdensServicoService } from './ordens-servico.service';
|
||||
|
||||
@Controller('ordens-servico')
|
||||
@@ -8,10 +10,43 @@ export class OrdensServicoController {
|
||||
@Get() findAll() { return this.svc.findAll(); }
|
||||
@Get(':id') findOne(@Param('id') id: string) { return this.svc.findOne(id); }
|
||||
@Post() create(@Body() body: any) { return this.svc.create(body); }
|
||||
@Patch(':id') update(@Param('id') id: string, @Body() body: any) { return this.svc.update(id, body); }
|
||||
@Delete(':id') remove(@Param('id') id: string) { return this.svc.remove(id); }
|
||||
|
||||
@Get('by-demanda/:demandaId') findByDemanda(@Param('demandaId') demandaId: string) { return this.svc.findByDemanda(demandaId); }
|
||||
|
||||
@Post(':id/iniciar') iniciar(@Param('id') id: string) { return this.svc.iniciar(id); }
|
||||
@Post(':id/concluir') concluir(@Param('id') id: string) { return this.svc.concluir(id); }
|
||||
@Post(':id/cancelar') cancelar(@Param('id') id: string) { return this.svc.cancelar(id); }
|
||||
@Post(':id/status') updateStatus(@Param('id') id: string, @Body('status') status: string) { return this.svc.updateStatus(id, status); }
|
||||
|
||||
@Post(':id/avaliacao') avaliacao(@Param('id') id: string, @Body() body: any) {
|
||||
return this.svc.createAvaliacao({ ...body, ordem_servico_id: id });
|
||||
}
|
||||
|
||||
// Document versioning
|
||||
@Get(':id/documentos') findDocumentos(@Param('id') id: string) { return this.svc.findDocumentos(id); }
|
||||
|
||||
@Post(':id/documentos')
|
||||
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 50 * 1024 * 1024 } }))
|
||||
uploadDocumento(
|
||||
@Param('id') id: string,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body('fornecedor_id') fornecedorId?: string,
|
||||
) {
|
||||
return this.svc.uploadDocumento(id, file, fornecedorId);
|
||||
}
|
||||
|
||||
@Get('documentos/:docId/download')
|
||||
async downloadDocumento(@Param('docId') docId: string, @Res() res: Response) {
|
||||
const { doc, filePath } = await this.svc.downloadDocumento(docId);
|
||||
res.download(filePath, doc.nome_arquivo);
|
||||
}
|
||||
|
||||
// Check fornecedor OS
|
||||
@Get('check-fornecedor/:fornecedorId')
|
||||
async checkFornecedor(@Param('fornecedorId') fornecedorId: string) {
|
||||
const hasOS = await this.svc.fornecedorHasOS(fornecedorId);
|
||||
return { hasOS };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MulterModule } from '@nestjs/platform-express';
|
||||
import { OrdemServico } from './entities/ordem-servico.entity';
|
||||
import { Avaliacao } from './entities/avaliacao.entity';
|
||||
import { DocumentoVersao } from './entities/documento-versao.entity';
|
||||
import { OrdensServicoController } from './ordens-servico.controller';
|
||||
import { OrdensServicoService } from './ordens-servico.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([OrdemServico, Avaliacao])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([OrdemServico, Avaliacao, DocumentoVersao]),
|
||||
MulterModule.register({ limits: { fileSize: 50 * 1024 * 1024 } }),
|
||||
],
|
||||
controllers: [OrdensServicoController],
|
||||
providers: [OrdensServicoService],
|
||||
exports: [OrdensServicoService],
|
||||
|
||||
@@ -3,21 +3,91 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { OrdemServico } from './entities/ordem-servico.entity';
|
||||
import { Avaliacao } from './entities/avaliacao.entity';
|
||||
import { DocumentoVersao } from './entities/documento-versao.entity';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/opt/hefesto/uploads';
|
||||
|
||||
@Injectable()
|
||||
export class OrdensServicoService {
|
||||
constructor(
|
||||
@InjectRepository(OrdemServico) private repo: Repository<OrdemServico>,
|
||||
@InjectRepository(Avaliacao) private avalRepo: Repository<Avaliacao>,
|
||||
) {}
|
||||
@InjectRepository(DocumentoVersao) private docRepo: Repository<DocumentoVersao>,
|
||||
) {
|
||||
if (!fs.existsSync(UPLOAD_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
findAll() { return this.repo.find(); }
|
||||
findAll() { return this.repo.find({ order: { created_at: 'DESC' } }); }
|
||||
findOne(id: string) { return this.repo.findOne({ where: { id } }); }
|
||||
create(data: Partial<OrdemServico>) { return this.repo.save(data); }
|
||||
|
||||
findByDemanda(demandaId: string) {
|
||||
return this.repo.find({ where: { demanda_id: demandaId }, order: { created_at: 'DESC' } });
|
||||
}
|
||||
|
||||
async create(data: Partial<OrdemServico>) {
|
||||
// Auto-number
|
||||
const last = await this.repo.query('SELECT MAX(numero) as max FROM ordens_servico');
|
||||
const nextNum = (last[0]?.max || 0) + 1;
|
||||
return this.repo.save({ ...data, numero: nextNum, status: data.status || 'emitida' });
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<OrdemServico>) {
|
||||
await this.repo.update(id, data);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async remove(id: string) { await this.repo.delete(id); return { deleted: true }; }
|
||||
|
||||
async iniciar(id: string) { await this.repo.update(id, { status: 'em_execucao', data_inicio: new Date().toISOString().split('T')[0] }); return this.findOne(id); }
|
||||
async concluir(id: string) { await this.repo.update(id, { status: 'concluida', data_conclusao: new Date().toISOString().split('T')[0] }); return this.findOne(id); }
|
||||
async cancelar(id: string) { await this.repo.update(id, { status: 'cancelada' }); return this.findOne(id); }
|
||||
async updateStatus(id: string, status: string) { await this.repo.update(id, { status }); return this.findOne(id); }
|
||||
|
||||
createAvaliacao(data: Partial<Avaliacao>) { return this.avalRepo.save(data); }
|
||||
|
||||
// Document versioning
|
||||
findDocumentos(osId: string) {
|
||||
return this.docRepo.find({ where: { ordem_servico_id: osId }, order: { versao: 'DESC' } });
|
||||
}
|
||||
|
||||
async uploadDocumento(osId: string, file: Express.Multer.File, fornecedorId?: string) {
|
||||
const ext = path.extname(file.originalname);
|
||||
const filename = `os_${osId}_${Date.now()}${ext}`;
|
||||
const destPath = path.join(UPLOAD_DIR, filename);
|
||||
fs.writeFileSync(destPath, file.buffer);
|
||||
|
||||
// Calculate next version
|
||||
const existing = await this.docRepo.find({
|
||||
where: { ordem_servico_id: osId, fornecedor_id: fornecedorId || undefined } as any,
|
||||
order: { versao: 'DESC' }
|
||||
});
|
||||
const nextVersion = existing.length > 0 ? existing[0].versao + 1 : 1;
|
||||
|
||||
const doc = await this.docRepo.save({
|
||||
ordem_servico_id: osId,
|
||||
fornecedor_id: fornecedorId,
|
||||
nome_arquivo: file.originalname,
|
||||
caminho: filename,
|
||||
tamanho: file.size,
|
||||
versao: nextVersion,
|
||||
});
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
async downloadDocumento(id: string) {
|
||||
const doc = await this.docRepo.findOne({ where: { id } });
|
||||
if (!doc) throw new Error('Documento não encontrado');
|
||||
return { doc, filePath: path.join(UPLOAD_DIR, doc.caminho) };
|
||||
}
|
||||
|
||||
// Check if fornecedor has OS
|
||||
async fornecedorHasOS(fornecedorId: string): Promise<boolean> {
|
||||
const count = await this.repo.count({ where: { fornecedor_id: fornecedorId } });
|
||||
return count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,15 @@ export class Proposta {
|
||||
@Column({ default: false })
|
||||
selecionada: boolean;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
uso_material_sustentavel: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
gera_residuos: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
descarte_certificado: string;
|
||||
|
||||
@Column({ length: 20, default: 'recebida' })
|
||||
status: string;
|
||||
|
||||
|
||||
35
backend/src/modules/relatorios/relatorios.controller.ts
Normal file
35
backend/src/modules/relatorios/relatorios.controller.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { RelatoriosService } from './relatorios.service';
|
||||
|
||||
@Controller('relatorios')
|
||||
export class RelatoriosController {
|
||||
constructor(private readonly svc: RelatoriosService) {}
|
||||
|
||||
@Get('orcamento-mensal')
|
||||
orcamentoMensal(@Query('ano') ano: string, @Query('mes') mes: string) {
|
||||
return this.svc.orcamentoMensal(Number(ano) || 2026, Number(mes) || 1);
|
||||
}
|
||||
|
||||
@Get('demandas-periodo')
|
||||
demandasPeriodo(@Query('de') de: string, @Query('ate') ate: string) {
|
||||
return this.svc.demandasPeriodo(de || '2026-01-01', ate || '2026-12-31');
|
||||
}
|
||||
|
||||
@Get('fornecedores-ranking')
|
||||
fornecedoresRanking() { return this.svc.fornecedoresRanking(); }
|
||||
|
||||
@Get('os-performance')
|
||||
osPerformance() { return this.svc.osPerformance(); }
|
||||
|
||||
@Get('esg-impacto-ambiental')
|
||||
esgImpactoAmbiental() { return this.svc.demandasPorImpactoAmbiental(); }
|
||||
|
||||
@Get('esg-fornecedores')
|
||||
esgFornecedores() { return this.svc.fornecedoresPorEsg(); }
|
||||
|
||||
@Get('esg-evolucao-preventiva')
|
||||
esgEvolucaoPreventiva() { return this.svc.evolucaoManutencaoPreventiva(); }
|
||||
|
||||
@Get('esg-excecoes-governanca')
|
||||
esgExcecoesGovernanca() { return this.svc.excecoesGovernanca(); }
|
||||
}
|
||||
9
backend/src/modules/relatorios/relatorios.module.ts
Normal file
9
backend/src/modules/relatorios/relatorios.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RelatoriosController } from './relatorios.controller';
|
||||
import { RelatoriosService } from './relatorios.service';
|
||||
|
||||
@Module({
|
||||
controllers: [RelatoriosController],
|
||||
providers: [RelatoriosService],
|
||||
})
|
||||
export class RelatoriosModule {}
|
||||
145
backend/src/modules/relatorios/relatorios.service.ts
Normal file
145
backend/src/modules/relatorios/relatorios.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class RelatoriosService {
|
||||
constructor(private ds: DataSource) {}
|
||||
|
||||
async orcamentoMensal(ano: number, mes: number) {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT op.*, cc.nome as centro_custo_nome, cat.nome as categoria_nome
|
||||
FROM orcamento_planejado op
|
||||
LEFT JOIN centros_custo cc ON cc.id::text = op.centro_custo_id::text
|
||||
LEFT JOIN categorias cat ON cat.id::text = op.categoria_id::text
|
||||
WHERE op.ano = $1 AND op.mes = $2
|
||||
ORDER BY cc.nome, cat.nome`, [ano, mes]);
|
||||
|
||||
const totais = rows.reduce((acc: any, r: any) => ({
|
||||
planejado: (acc.planejado || 0) + (r.valor_planejado || 0),
|
||||
comprometido: (acc.comprometido || 0) + (r.valor_comprometido || 0),
|
||||
realizado: (acc.realizado || 0) + (r.valor_realizado || 0),
|
||||
}), {});
|
||||
|
||||
return { ano, mes, itens: rows, totais };
|
||||
}
|
||||
|
||||
async demandasPeriodo(de: string, ate: string) {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT d.*, cc.nome as centro_custo_nome, l.nome as local_nome
|
||||
FROM demandas d
|
||||
LEFT JOIN centros_custo cc ON cc.id::text = d.centro_custo_id::text
|
||||
LEFT JOIN locais l ON l.id::text = d.local_id::text
|
||||
WHERE d.created_at >= $1 AND d.created_at <= $2
|
||||
ORDER BY d.created_at DESC`, [de, ate + ' 23:59:59']);
|
||||
|
||||
const por_status = await this.ds.query(`
|
||||
SELECT status, COUNT(*) as total FROM demandas
|
||||
WHERE created_at >= $1 AND created_at <= $2
|
||||
GROUP BY status`, [de, ate + ' 23:59:59']);
|
||||
|
||||
return { periodo: { de, ate }, demandas: rows, resumo_por_status: por_status, total: rows.length };
|
||||
}
|
||||
|
||||
async fornecedoresRanking() {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT f.id, f.razao_social, f.nome_fantasia, f.rating,
|
||||
COUNT(DISTINCT os.id) as total_os,
|
||||
COALESCE(AVG(av.nota), 0) as nota_media,
|
||||
COALESCE(SUM(os.valor), 0) as valor_total_os
|
||||
FROM fornecedores f
|
||||
LEFT JOIN ordens_servico os ON os.fornecedor_id::text = f.id::text
|
||||
LEFT JOIN avaliacoes av ON av.fornecedor_id::text = f.id::text
|
||||
WHERE f.ativo = true
|
||||
GROUP BY f.id, f.razao_social, f.nome_fantasia, f.rating
|
||||
ORDER BY nota_media DESC, total_os DESC`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async demandasPorImpactoAmbiental() {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT d.impacto_ambiental_demanda as impacto, COUNT(*) as total,
|
||||
d.status, c.nome as categoria
|
||||
FROM demandas d
|
||||
LEFT JOIN categorias c ON c.id::text = d.categoria_id::text
|
||||
WHERE d.impacto_ambiental_demanda IS NOT NULL
|
||||
GROUP BY d.impacto_ambiental_demanda, d.status, c.nome
|
||||
ORDER BY d.impacto_ambiental_demanda, d.status`);
|
||||
|
||||
const resumo = await this.ds.query(`
|
||||
SELECT impacto_ambiental_demanda as impacto, COUNT(*) as total
|
||||
FROM demandas WHERE impacto_ambiental_demanda IS NOT NULL
|
||||
GROUP BY impacto_ambiental_demanda`);
|
||||
|
||||
return { detalhes: rows, resumo };
|
||||
}
|
||||
|
||||
async fornecedoresPorEsg() {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT f.id, f.razao_social, f.nome_fantasia, f.classificacao_esg,
|
||||
f.possui_politica_ambiental, f.possui_politica_sst, f.declara_uso_epi, f.equipe_treinada,
|
||||
f.rating, COUNT(DISTINCT os.id) as total_os
|
||||
FROM fornecedores f
|
||||
LEFT JOIN ordens_servico os ON os.fornecedor_id::text = f.id::text
|
||||
WHERE f.ativo = true
|
||||
GROUP BY f.id, f.razao_social, f.nome_fantasia, f.classificacao_esg,
|
||||
f.possui_politica_ambiental, f.possui_politica_sst, f.declara_uso_epi, f.equipe_treinada, f.rating
|
||||
ORDER BY f.classificacao_esg DESC, f.rating DESC`);
|
||||
|
||||
const resumo = await this.ds.query(`
|
||||
SELECT classificacao_esg, COUNT(*) as total
|
||||
FROM fornecedores WHERE ativo = true AND classificacao_esg IS NOT NULL
|
||||
GROUP BY classificacao_esg`);
|
||||
|
||||
return { fornecedores: rows, resumo };
|
||||
}
|
||||
|
||||
async evolucaoManutencaoPreventiva() {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT EXTRACT(MONTH FROM d.created_at) as mes, EXTRACT(YEAR FROM d.created_at) as ano,
|
||||
c.tipo_manutencao, COUNT(*) as total
|
||||
FROM demandas d
|
||||
LEFT JOIN categorias c ON c.id::text = d.categoria_id::text
|
||||
WHERE c.tipo_manutencao IS NOT NULL
|
||||
GROUP BY EXTRACT(MONTH FROM d.created_at), EXTRACT(YEAR FROM d.created_at), c.tipo_manutencao
|
||||
ORDER BY ano, mes`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async excecoesGovernanca() {
|
||||
const rows = await this.ds.query(`
|
||||
SELECT w.id, w.demanda_id, w.valor_total, w.status, w.etapas,
|
||||
d.titulo as demanda_titulo, d.numero as demanda_numero
|
||||
FROM workflow_aprovacoes w
|
||||
LEFT JOIN demandas d ON d.id::text = w.demanda_id::text
|
||||
ORDER BY w.created_at DESC`);
|
||||
|
||||
// Filter workflows that have ressalva in any step
|
||||
const comRessalva = rows.filter((w: any) => {
|
||||
try {
|
||||
const etapas = typeof w.etapas === 'string' ? JSON.parse(w.etapas) : w.etapas;
|
||||
return Array.isArray(etapas) && etapas.some((e: any) => e.ressalva || e.status === 'aprovado_com_ressalva');
|
||||
} catch { return false; }
|
||||
});
|
||||
|
||||
return { total: comRessalva.length, itens: comRessalva };
|
||||
}
|
||||
|
||||
async osPerformance() {
|
||||
const por_status = await this.ds.query(`
|
||||
SELECT status, COUNT(*) as total, COALESCE(SUM(valor),0) as valor_total
|
||||
FROM ordens_servico GROUP BY status`);
|
||||
|
||||
const tempo_medio = await this.ds.query(`
|
||||
SELECT AVG(data_conclusao::date - data_inicio::date) as dias_medio
|
||||
FROM ordens_servico WHERE status = 'concluida' AND data_inicio IS NOT NULL AND data_conclusao IS NOT NULL`);
|
||||
|
||||
const por_fornecedor = await this.ds.query(`
|
||||
SELECT f.razao_social, COUNT(os.id) as total, AVG(os.data_conclusao::date - os.data_inicio::date) as dias_medio
|
||||
FROM ordens_servico os
|
||||
LEFT JOIN fornecedores f ON f.id::text = os.fornecedor_id::text
|
||||
GROUP BY os.fornecedor_id, f.razao_social
|
||||
ORDER BY total DESC`);
|
||||
|
||||
return { por_status, tempo_medio_dias: tempo_medio[0]?.dias_medio || null, por_fornecedor };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('subcategorias')
|
||||
export class Subcategoria {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 200 })
|
||||
nome: string;
|
||||
|
||||
@Column()
|
||||
categoria_id: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
tipo_manutencao: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
impacto_ambiental_esperado: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
potencial_geracao_residuos: string;
|
||||
|
||||
@Column({ default: true })
|
||||
ativo: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Controller, Get, Post, Patch, Delete, Body, Param, Query } from '@nestjs/common';
|
||||
import { SubcategoriasService } from './subcategorias.service';
|
||||
|
||||
@Controller('subcategorias')
|
||||
export class SubcategoriasController {
|
||||
constructor(private svc: SubcategoriasService) {}
|
||||
|
||||
@Get() findAll(@Query('categoria_id') categoriaId?: string) { return this.svc.findAll(categoriaId); }
|
||||
@Get(':id') findOne(@Param('id') id: string) { return this.svc.findOne(id); }
|
||||
@Post() create(@Body() body: any) { return this.svc.create(body); }
|
||||
@Patch(':id') update(@Param('id') id: string, @Body() body: any) { return this.svc.update(id, body); }
|
||||
@Delete(':id') remove(@Param('id') id: string) { return this.svc.remove(id); }
|
||||
}
|
||||
13
backend/src/modules/subcategorias/subcategorias.module.ts
Normal file
13
backend/src/modules/subcategorias/subcategorias.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Subcategoria } from './entities/subcategoria.entity';
|
||||
import { SubcategoriasController } from './subcategorias.controller';
|
||||
import { SubcategoriasService } from './subcategorias.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Subcategoria])],
|
||||
controllers: [SubcategoriasController],
|
||||
providers: [SubcategoriasService],
|
||||
exports: [SubcategoriasService],
|
||||
})
|
||||
export class SubcategoriasModule {}
|
||||
20
backend/src/modules/subcategorias/subcategorias.service.ts
Normal file
20
backend/src/modules/subcategorias/subcategorias.service.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Subcategoria } from './entities/subcategoria.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SubcategoriasService {
|
||||
constructor(@InjectRepository(Subcategoria) private repo: Repository<Subcategoria>) {}
|
||||
|
||||
findAll(categoriaId?: string) {
|
||||
const where: any = { ativo: true };
|
||||
if (categoriaId) where.categoria_id = categoriaId;
|
||||
return this.repo.find({ where, order: { nome: 'ASC' } });
|
||||
}
|
||||
|
||||
findOne(id: string) { return this.repo.findOne({ where: { id } }); }
|
||||
create(data: Partial<Subcategoria>) { return this.repo.save(data); }
|
||||
async update(id: string, data: Partial<Subcategoria>) { await this.repo.update(id, data); return this.findOne(id); }
|
||||
async remove(id: string) { await this.repo.update(id, { ativo: false }); }
|
||||
}
|
||||
Reference in New Issue
Block a user