Major update: ESG, KPIs, metas, alertas, auditoria, documentos, importação, relatórios, subcategorias, dashboard orçamentos

This commit is contained in:
bigtux
2026-02-10 18:52:52 -03:00
parent d8ca580acb
commit 90c7a2cacb
92 changed files with 10265 additions and 1238 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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],
})

View File

@@ -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!');
}
}

View File

@@ -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();

View 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(); }
}

View 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 {}

View 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 };
}
}

View 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;
}

View 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);
}
}

View 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 {}

View 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();
}
}

View File

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

View File

@@ -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(); }
}

View File

@@ -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],

View File

@@ -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,
};
}
}

View File

@@ -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'); }

View File

@@ -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 });

View File

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

View 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);
}
}

View 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 {}

View 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);
}
}

View 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;
}

View 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;
}

View 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;
}

View 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); }
}

View 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 {}

View 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 };
}
}

View File

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

View File

@@ -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); }

View 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);
}
}

View 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 {}

View 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 };
}
}

View 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;
}

View 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); }
}

View 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 {}

View 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 };
}
}

View File

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

View 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;
}

View 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(); }
}

View 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 {}

View 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 };
}
}

View File

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

View File

@@ -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); }
}

View File

@@ -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],

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

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

View File

@@ -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 };
}
}

View File

@@ -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],

View File

@@ -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;
}
}

View File

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

View 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(); }
}

View 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 {}

View 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 };
}
}

View File

@@ -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;
}

View File

@@ -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); }
}

View 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 {}

View 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 }); }
}