diff --git a/backend/package-lock.json b/backend/package-lock.json index ef545db..1c7d9a8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index 7478b00..1d3488b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 29950a1..d756ea3 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -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], }) diff --git a/backend/src/database/seeds/seed.service.ts b/backend/src/database/seeds/seed.service.ts index c120cd6..3398ce7 100644 --- a/backend/src/database/seeds/seed.service.ts +++ b/backend/src/database/seeds/seed.service.ts @@ -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, @InjectRepository(OrdemServico) private osRepo: Repository, @InjectRepository(Alerta) private alertaRepo: Repository, + @InjectRepository(EsgMetrica) private esgMetricaRepo: Repository, + @InjectRepository(EsgMeta) private esgMetaRepo: Repository, + @InjectRepository(Kpi) private kpiRepo: Repository, + @InjectRepository(Meta) private metaRepo: Repository, + @InjectRepository(AlertaConfig) private alertaConfigRepo: Repository, + @InjectRepository(Subcategoria) private subcatRepo: Repository, ) {} 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!'); } } diff --git a/backend/src/main.ts b/backend/src/main.ts index 4b00585..cfd3451 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -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(); diff --git a/backend/src/modules/alertas/alertas.controller.ts b/backend/src/modules/alertas/alertas.controller.ts new file mode 100644 index 0000000..bf112e7 --- /dev/null +++ b/backend/src/modules/alertas/alertas.controller.ts @@ -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(); } +} diff --git a/backend/src/modules/alertas/alertas.module.ts b/backend/src/modules/alertas/alertas.module.ts new file mode 100644 index 0000000..b205a34 --- /dev/null +++ b/backend/src/modules/alertas/alertas.module.ts @@ -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 {} diff --git a/backend/src/modules/alertas/alertas.service.ts b/backend/src/modules/alertas/alertas.service.ts new file mode 100644 index 0000000..676e51a --- /dev/null +++ b/backend/src/modules/alertas/alertas.service.ts @@ -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, + @InjectRepository(Alerta) private alertaRepo: Repository, + 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 }; + } +} diff --git a/backend/src/modules/alertas/entities/alerta-config.entity.ts b/backend/src/modules/alertas/entities/alerta-config.entity.ts new file mode 100644 index 0000000..f978440 --- /dev/null +++ b/backend/src/modules/alertas/entities/alerta-config.entity.ts @@ -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; +} diff --git a/backend/src/modules/audit/audit.controller.ts b/backend/src/modules/audit/audit.controller.ts new file mode 100644 index 0000000..5bd724c --- /dev/null +++ b/backend/src/modules/audit/audit.controller.ts @@ -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); + } +} diff --git a/backend/src/modules/audit/audit.module.ts b/backend/src/modules/audit/audit.module.ts new file mode 100644 index 0000000..445f02e --- /dev/null +++ b/backend/src/modules/audit/audit.module.ts @@ -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 {} diff --git a/backend/src/modules/audit/audit.service.ts b/backend/src/modules/audit/audit.service.ts new file mode 100644 index 0000000..1548b00 --- /dev/null +++ b/backend/src/modules/audit/audit.service.ts @@ -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, + ) {} + + 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(); + } +} diff --git a/backend/src/modules/categorias/entities/categoria.entity.ts b/backend/src/modules/categorias/entities/categoria.entity.ts index 249f08d..7a8e3c1 100644 --- a/backend/src/modules/categorias/entities/categoria.entity.ts +++ b/backend/src/modules/categorias/entities/categoria.entity.ts @@ -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; diff --git a/backend/src/modules/dashboard/dashboard.controller.ts b/backend/src/modules/dashboard/dashboard.controller.ts index e520cb2..dc99aa3 100644 --- a/backend/src/modules/dashboard/dashboard.controller.ts +++ b/backend/src/modules/dashboard/dashboard.controller.ts @@ -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(); } } diff --git a/backend/src/modules/dashboard/dashboard.module.ts b/backend/src/modules/dashboard/dashboard.module.ts index 824aaec..1fd9827 100644 --- a/backend/src/modules/dashboard/dashboard.module.ts +++ b/backend/src/modules/dashboard/dashboard.module.ts @@ -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], diff --git a/backend/src/modules/dashboard/dashboard.service.ts b/backend/src/modules/dashboard/dashboard.service.ts index 9647eb1..f70eab6 100644 --- a/backend/src/modules/dashboard/dashboard.service.ts +++ b/backend/src/modules/dashboard/dashboard.service.ts @@ -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, @InjectRepository(WorkflowAprovacao) private wfRepo: Repository, @InjectRepository(Alerta) private alertaRepo: Repository, + @InjectRepository(Fornecedor) private fornecedorRepo: Repository, + @InjectRepository(Categoria) private categoriaRepo: Repository, + 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 = {}; 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 = {}; + 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 = {}; + categorias.forEach(c => { catMap[c.id] = c.nome; }); + + const counts: Record = {}; + 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 = {}; + 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 = {}; + 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 = {}; + 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, + }; + } } diff --git a/backend/src/modules/demandas/demandas.controller.ts b/backend/src/modules/demandas/demandas.controller.ts index 727f109..6fa4054 100644 --- a/backend/src/modules/demandas/demandas.controller.ts +++ b/backend/src/modules/demandas/demandas.controller.ts @@ -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'); } diff --git a/backend/src/modules/demandas/demandas.service.ts b/backend/src/modules/demandas/demandas.service.ts index 9f828d0..61f1333 100644 --- a/backend/src/modules/demandas/demandas.service.ts +++ b/backend/src/modules/demandas/demandas.service.ts @@ -22,6 +22,7 @@ export class DemandasService { findOne(id: string) { return this.repo.findOne({ where: { id }, relations: ['itens_linha'] }); } create(data: Partial) { return this.repo.save(data); } async update(id: string, data: Partial) { 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 }); diff --git a/backend/src/modules/demandas/entities/demanda.entity.ts b/backend/src/modules/demandas/entities/demanda.entity.ts index 6b83ae0..58074fb 100644 --- a/backend/src/modules/demandas/entities/demanda.entity.ts +++ b/backend/src/modules/demandas/entities/demanda.entity.ts @@ -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; diff --git a/backend/src/modules/documentos/documentos.controller.ts b/backend/src/modules/documentos/documentos.controller.ts new file mode 100644 index 0000000..e65e7ea --- /dev/null +++ b/backend/src/modules/documentos/documentos.controller.ts @@ -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); + } +} diff --git a/backend/src/modules/documentos/documentos.module.ts b/backend/src/modules/documentos/documentos.module.ts new file mode 100644 index 0000000..536d193 --- /dev/null +++ b/backend/src/modules/documentos/documentos.module.ts @@ -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 {} diff --git a/backend/src/modules/documentos/documentos.service.ts b/backend/src/modules/documentos/documentos.service.ts new file mode 100644 index 0000000..a0f63ad --- /dev/null +++ b/backend/src/modules/documentos/documentos.service.ts @@ -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) { + // 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); + } +} diff --git a/backend/src/modules/documentos/entities/documento.entity.ts b/backend/src/modules/documentos/entities/documento.entity.ts new file mode 100644 index 0000000..b4e7486 --- /dev/null +++ b/backend/src/modules/documentos/entities/documento.entity.ts @@ -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; +} diff --git a/backend/src/modules/esg/entities/esg-meta.entity.ts b/backend/src/modules/esg/entities/esg-meta.entity.ts new file mode 100644 index 0000000..8a4ed2b --- /dev/null +++ b/backend/src/modules/esg/entities/esg-meta.entity.ts @@ -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; +} diff --git a/backend/src/modules/esg/entities/esg-metrica.entity.ts b/backend/src/modules/esg/entities/esg-metrica.entity.ts new file mode 100644 index 0000000..d2f01ac --- /dev/null +++ b/backend/src/modules/esg/entities/esg-metrica.entity.ts @@ -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; +} diff --git a/backend/src/modules/esg/esg.controller.ts b/backend/src/modules/esg/esg.controller.ts new file mode 100644 index 0000000..d120ca5 --- /dev/null +++ b/backend/src/modules/esg/esg.controller.ts @@ -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); } +} diff --git a/backend/src/modules/esg/esg.module.ts b/backend/src/modules/esg/esg.module.ts new file mode 100644 index 0000000..673a8c1 --- /dev/null +++ b/backend/src/modules/esg/esg.module.ts @@ -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 {} diff --git a/backend/src/modules/esg/esg.service.ts b/backend/src/modules/esg/esg.service.ts new file mode 100644 index 0000000..f9c9607 --- /dev/null +++ b/backend/src/modules/esg/esg.service.ts @@ -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, + @InjectRepository(EsgMeta) private metaRepo: Repository, + ) {} + + 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 }; + } +} diff --git a/backend/src/modules/fornecedores/entities/fornecedor.entity.ts b/backend/src/modules/fornecedores/entities/fornecedor.entity.ts index 8d4d292..d9534de 100644 --- a/backend/src/modules/fornecedores/entities/fornecedor.entity.ts +++ b/backend/src/modules/fornecedores/entities/fornecedor.entity.ts @@ -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; diff --git a/backend/src/modules/fornecedores/fornecedores.service.ts b/backend/src/modules/fornecedores/fornecedores.service.ts index 053f4f1..1224576 100644 --- a/backend/src/modules/fornecedores/fornecedores.service.ts +++ b/backend/src/modules/fornecedores/fornecedores.service.ts @@ -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) { return this.repo.save(data); } async update(id: string, data: Partial) { 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) { return this.certRepo.save(data); } diff --git a/backend/src/modules/import/import.controller.ts b/backend/src/modules/import/import.controller.ts new file mode 100644 index 0000000..0a2b27a --- /dev/null +++ b/backend/src/modules/import/import.controller.ts @@ -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); + } +} diff --git a/backend/src/modules/import/import.module.ts b/backend/src/modules/import/import.module.ts new file mode 100644 index 0000000..93df18f --- /dev/null +++ b/backend/src/modules/import/import.module.ts @@ -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 {} diff --git a/backend/src/modules/import/import.service.ts b/backend/src/modules/import/import.service.ts new file mode 100644 index 0000000..ee081d6 --- /dev/null +++ b/backend/src/modules/import/import.service.ts @@ -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 }; + } +} diff --git a/backend/src/modules/kpis/entities/kpi.entity.ts b/backend/src/modules/kpis/entities/kpi.entity.ts new file mode 100644 index 0000000..3241437 --- /dev/null +++ b/backend/src/modules/kpis/entities/kpi.entity.ts @@ -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; +} diff --git a/backend/src/modules/kpis/kpis.controller.ts b/backend/src/modules/kpis/kpis.controller.ts new file mode 100644 index 0000000..bf1c726 --- /dev/null +++ b/backend/src/modules/kpis/kpis.controller.ts @@ -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); } +} diff --git a/backend/src/modules/kpis/kpis.module.ts b/backend/src/modules/kpis/kpis.module.ts new file mode 100644 index 0000000..bcac838 --- /dev/null +++ b/backend/src/modules/kpis/kpis.module.ts @@ -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 {} diff --git a/backend/src/modules/kpis/kpis.service.ts b/backend/src/modules/kpis/kpis.service.ts new file mode 100644 index 0000000..ccde020 --- /dev/null +++ b/backend/src/modules/kpis/kpis.service.ts @@ -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, + 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 }; + } +} diff --git a/backend/src/modules/locais/entities/local.entity.ts b/backend/src/modules/locais/entities/local.entity.ts index 5d1810d..5c91e9f 100644 --- a/backend/src/modules/locais/entities/local.entity.ts +++ b/backend/src/modules/locais/entities/local.entity.ts @@ -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; diff --git a/backend/src/modules/metas/entities/meta.entity.ts b/backend/src/modules/metas/entities/meta.entity.ts new file mode 100644 index 0000000..fbafa92 --- /dev/null +++ b/backend/src/modules/metas/entities/meta.entity.ts @@ -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; +} diff --git a/backend/src/modules/metas/metas.controller.ts b/backend/src/modules/metas/metas.controller.ts new file mode 100644 index 0000000..937fd8d --- /dev/null +++ b/backend/src/modules/metas/metas.controller.ts @@ -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(); } +} diff --git a/backend/src/modules/metas/metas.module.ts b/backend/src/modules/metas/metas.module.ts new file mode 100644 index 0000000..a2cde79 --- /dev/null +++ b/backend/src/modules/metas/metas.module.ts @@ -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 {} diff --git a/backend/src/modules/metas/metas.service.ts b/backend/src/modules/metas/metas.service.ts new file mode 100644 index 0000000..e647d95 --- /dev/null +++ b/backend/src/modules/metas/metas.service.ts @@ -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) {} + + 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 }; + } +} diff --git a/backend/src/modules/orcamento/entities/orcamento-planejado.entity.ts b/backend/src/modules/orcamento/entities/orcamento-planejado.entity.ts index bafab66..6ecf418 100644 --- a/backend/src/modules/orcamento/entities/orcamento-planejado.entity.ts +++ b/backend/src/modules/orcamento/entities/orcamento-planejado.entity.ts @@ -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; diff --git a/backend/src/modules/orcamento/orcamento.controller.ts b/backend/src/modules/orcamento/orcamento.controller.ts index 0646c63..813956e 100644 --- a/backend/src/modules/orcamento/orcamento.controller.ts +++ b/backend/src/modules/orcamento/orcamento.controller.ts @@ -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); } } diff --git a/backend/src/modules/orcamento/orcamento.module.ts b/backend/src/modules/orcamento/orcamento.module.ts index 0cc131b..3df9612 100644 --- a/backend/src/modules/orcamento/orcamento.module.ts +++ b/backend/src/modules/orcamento/orcamento.module.ts @@ -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], diff --git a/backend/src/modules/orcamento/orcamento.service.ts b/backend/src/modules/orcamento/orcamento.service.ts index f3e5983..97e83ee 100644 --- a/backend/src/modules/orcamento/orcamento.service.ts +++ b/backend/src/modules/orcamento/orcamento.service.ts @@ -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) {} + constructor( + @InjectRepository(OrcamentoPlanejado) private repo: Repository, + @InjectRepository(Categoria) private categoriaRepo: Repository, + ) {} - 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 = {}; + 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) { return this.repo.save(data); } async update(id: string, data: Partial) { 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 = {}; + categorias.forEach(c => { catMap[c.id] = c.tipo_investimento || 'Não definido'; }); + + const result: Record = {}; + 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); + } } diff --git a/backend/src/modules/ordens-servico/entities/documento-versao.entity.ts b/backend/src/modules/ordens-servico/entities/documento-versao.entity.ts new file mode 100644 index 0000000..eed179a --- /dev/null +++ b/backend/src/modules/ordens-servico/entities/documento-versao.entity.ts @@ -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; +} diff --git a/backend/src/modules/ordens-servico/entities/ordem-servico.entity.ts b/backend/src/modules/ordens-servico/entities/ordem-servico.entity.ts index b85201f..65096e3 100644 --- a/backend/src/modules/ordens-servico/entities/ordem-servico.entity.ts +++ b/backend/src/modules/ordens-servico/entities/ordem-servico.entity.ts @@ -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; diff --git a/backend/src/modules/ordens-servico/ordens-servico.controller.ts b/backend/src/modules/ordens-servico/ordens-servico.controller.ts index 36df928..d69ba3c 100644 --- a/backend/src/modules/ordens-servico/ordens-servico.controller.ts +++ b/backend/src/modules/ordens-servico/ordens-servico.controller.ts @@ -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 }; + } } diff --git a/backend/src/modules/ordens-servico/ordens-servico.module.ts b/backend/src/modules/ordens-servico/ordens-servico.module.ts index 05e45cd..fb13190 100644 --- a/backend/src/modules/ordens-servico/ordens-servico.module.ts +++ b/backend/src/modules/ordens-servico/ordens-servico.module.ts @@ -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], diff --git a/backend/src/modules/ordens-servico/ordens-servico.service.ts b/backend/src/modules/ordens-servico/ordens-servico.service.ts index d43a9a6..d02b9fd 100644 --- a/backend/src/modules/ordens-servico/ordens-servico.service.ts +++ b/backend/src/modules/ordens-servico/ordens-servico.service.ts @@ -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, @InjectRepository(Avaliacao) private avalRepo: Repository, - ) {} + @InjectRepository(DocumentoVersao) private docRepo: Repository, + ) { + 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) { return this.repo.save(data); } + + findByDemanda(demandaId: string) { + return this.repo.find({ where: { demanda_id: demandaId }, order: { created_at: 'DESC' } }); + } + + async create(data: Partial) { + // 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) { + 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) { 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 { + const count = await this.repo.count({ where: { fornecedor_id: fornecedorId } }); + return count > 0; + } } diff --git a/backend/src/modules/propostas/entities/proposta.entity.ts b/backend/src/modules/propostas/entities/proposta.entity.ts index b9bc570..549462c 100644 --- a/backend/src/modules/propostas/entities/proposta.entity.ts +++ b/backend/src/modules/propostas/entities/proposta.entity.ts @@ -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; diff --git a/backend/src/modules/relatorios/relatorios.controller.ts b/backend/src/modules/relatorios/relatorios.controller.ts new file mode 100644 index 0000000..85617a3 --- /dev/null +++ b/backend/src/modules/relatorios/relatorios.controller.ts @@ -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(); } +} diff --git a/backend/src/modules/relatorios/relatorios.module.ts b/backend/src/modules/relatorios/relatorios.module.ts new file mode 100644 index 0000000..d0d1c9d --- /dev/null +++ b/backend/src/modules/relatorios/relatorios.module.ts @@ -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 {} diff --git a/backend/src/modules/relatorios/relatorios.service.ts b/backend/src/modules/relatorios/relatorios.service.ts new file mode 100644 index 0000000..fbdffcb --- /dev/null +++ b/backend/src/modules/relatorios/relatorios.service.ts @@ -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 }; + } +} diff --git a/backend/src/modules/subcategorias/entities/subcategoria.entity.ts b/backend/src/modules/subcategorias/entities/subcategoria.entity.ts new file mode 100644 index 0000000..92b2bf4 --- /dev/null +++ b/backend/src/modules/subcategorias/entities/subcategoria.entity.ts @@ -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; +} diff --git a/backend/src/modules/subcategorias/subcategorias.controller.ts b/backend/src/modules/subcategorias/subcategorias.controller.ts new file mode 100644 index 0000000..b265e0c --- /dev/null +++ b/backend/src/modules/subcategorias/subcategorias.controller.ts @@ -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); } +} diff --git a/backend/src/modules/subcategorias/subcategorias.module.ts b/backend/src/modules/subcategorias/subcategorias.module.ts new file mode 100644 index 0000000..02456ee --- /dev/null +++ b/backend/src/modules/subcategorias/subcategorias.module.ts @@ -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 {} diff --git a/backend/src/modules/subcategorias/subcategorias.service.ts b/backend/src/modules/subcategorias/subcategorias.service.ts new file mode 100644 index 0000000..707cf88 --- /dev/null +++ b/backend/src/modules/subcategorias/subcategorias.service.ts @@ -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) {} + + 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) { return this.repo.save(data); } + async update(id: string, data: Partial) { await this.repo.update(id, data); return this.findOne(id); } + async remove(id: string) { await this.repo.update(id, { ativo: false }); } +} diff --git a/docs/DELTA-DASHBOARD-ORCAMENTOS.md b/docs/DELTA-DASHBOARD-ORCAMENTOS.md new file mode 100644 index 0000000..358b2ed --- /dev/null +++ b/docs/DELTA-DASHBOARD-ORCAMENTOS.md @@ -0,0 +1,26 @@ +# Delta Dashboard + Orçamentos — Nexus Facilities +> Especificação recebida em 2026-02-09 + +## 1. Tela Principal (Dashboard) + +### 1.1 Gráfico Orçamento vs Realizado +- Incluir filtro: Centro de Custo | Categoria | Todos +- Opção "Todos" = visão macro + +### 1.2 Cards de Demandas (Abertas, Em Cotação, Em Aprovação, Concluídas) +- Ao clicar no card, abrir lista das demandas naquele status +- Drill-down funcional + +### 1.3 Gráfico por Categoria +- BUG: não está trazendo as quantidades associadas — corrigir + +### 1.4 Campo Busca +- BUG: não está funcionando — corrigir + +## 2. Tela de Orçamentos + +### 2.1 Gráficos por Tipo de Investimento +- Mostrar total planejado, total realizado e economia separados por Capex e Opex + +### 2.2 Grid +- Incluir coluna "Tipo de Investimento" (Capex/Opex, vem da categoria) diff --git a/docs/DELTA-ESG.md b/docs/DELTA-ESG.md new file mode 100644 index 0000000..a6e16e2 --- /dev/null +++ b/docs/DELTA-ESG.md @@ -0,0 +1,60 @@ +# Delta ESG — Nexus Facilities +> Especificação recebida em 2026-02-09 + +## 1. Cadastros Auxiliares + +### 1.1 Local — ALTERAÇÃO +- `tipo_operacao_local`: Administrativo | Industrial | Logístico | Comercial +- `classificacao_impacto_ambiental`: Baixo | Médio | Alto +- `praticas_sustentaveis`: Lista (Coleta Seletiva, Reuso de Água, Energia Renovável) +- **Regra:** Demandas em locais Alto Impacto → sinalizar no Dashboard + aprovação adicional (matriz alçada) + +### 1.2 Categorias/Subcategorias — ALTERAÇÃO +- `tipo_manutencao`: Preventiva | Corretiva | Emergencial +- `impacto_ambiental_esperado`: Baixo | Médio | Alto +- `potencial_geracao_residuos`: Baixo | Médio | Alto +- **Regra:** Emergenciais + Alto Impacto → destaque em relatórios/indicadores + +### 1.3 Fornecedores — ALTERAÇÃO +- `possui_politica_ambiental`: Sim | Não +- `possui_politica_sst`: Sim | Não +- `declara_uso_epi`: Sim | Não +- `equipe_treinada`: Sim | Não +- `classificacao_esg`: Básico | Intermediário | Avançado +- **Regra:** ESG Básico → sinalizar reavaliação em demandas críticas + +### 1.4 Matriz de Aprovação (Alçadas) — ALTERAÇÃO +- `exige_avaliacao_esg`: Sim | Não +- **Regra:** Se Sim + (Impacto Alto OU Fornecedor ESG Básico) → justificativa adicional na aprovação + +## 2. Gestão de Demanda — ALTERAÇÃO +- `impacto_ambiental_demanda`: Herdado da Categoria (editável) +- `justificativa_manutencao_emergencial`: Obrigatório se tipo_manutencao = Emergencial + +## 3. Proposta Recebida — ALTERAÇÃO +- `uso_material_sustentavel`: Sim | Não | Não Informado +- `gera_residuos`: Baixo | Médio | Alto +- `descarte_certificado`: Sim | Não | Não Informado +- **Regra:** Extração automática via OCR quando possível, preenchimento manual na equalização (não obrigatório) + +## 4. Painel Comparação/Equalização — ALTERAÇÃO +- Mostrar Classificação ESG do Fornecedor +- Indicador visual de Impacto Ambiental da Demanda +- **Recomendação automática:** menor valor + match escopo + ESG fornecedor (secundário) + +## 5. Dashboard — INCLUSÃO +- % Demandas Preventivas vs Corretivas +- % Demandas Emergenciais +- % Fornecedores ESG Intermediário/Avançado +- Demandas com Impacto Ambiental Alto +- Evolução mensal manutenção preventiva + +## 6. Relatórios/Auditoria — ALTERAÇÃO +- Demandas por Impacto Ambiental +- Fornecedores por Classificação ESG +- Evolução Manutenção Preventiva +- Exceções de Governança (aprovações com ressalva) + +## 7. Regras Gerais de Governança +- Alto Impacto Ambiental → justificativa obrigatória registrada +- Dados ESG auditáveis e versionados diff --git a/docs/ESPECIFICACAO-FUNCIONAL.md b/docs/ESPECIFICACAO-FUNCIONAL.md new file mode 100644 index 0000000..4de3e9f --- /dev/null +++ b/docs/ESPECIFICACAO-FUNCIONAL.md @@ -0,0 +1,215 @@ +# Especificação Funcional — Sistema de Controle Orçamentário para Facilities (HEFESTO) + +## 1. Objetivo do Sistema +Desenvolver um sistema integrado para gestão de demandas de Facilities, com foco em controle orçamentário, comparação de propostas, governança financeira, compliance e apoio à tomada de decisão. + +O sistema deverá: +- Centralizar demandas de Facilities +- Controlar orçamento planejado x comprometido x realizado +- Automatizar comparação e equalização de propostas +- Suportar aprovações por alçada e fluxo sequencial +- Garantir rastreabilidade, auditoria e histórico decisório + +## 2. Perfis de Usuário + +| Perfil | Descrição | Principais Permissões | +|--------|-----------|----------------------| +| Solicitante | Área demandante | Abertura e acompanhamento de demandas | +| Gestor de Facilities | Responsável operacional | Criar escopo, validar propostas, interagir com fornecedores | +| Aprovador Financeiro | Controle orçamentário | Aprovar/reprovar propostas conforme alçada | +| Diretoria | Aprovação estratégica | Aprovação de alto valor / exceções | +| Fornecedor | Prestador de serviço | Envio e revisão de propostas | +| Administrador | Governança do sistema | Cadastros, parâmetros e regras | + +### 2.1 Dashboard Inicial (Home) +O Dashboard é a tela inicial do sistema, oferecendo uma visão executiva e operacional em tempo real, adaptada conforme o perfil do usuário. + +#### 2.1.1 Indicadores Principais (Cards) +- Demandas Abertas +- Demandas em Cotação +- Propostas Pendentes de Avaliação +- Demandas em Aprovação +- Ordens de Serviço Ativas +- Demandas com Alerta de Orçamento + +Cada card deve permitir drill-down para a lista filtrada correspondente. + +#### 2.1.2 Visões Gráficas +- Demandas por Status (Aberta, Em Cotação, Em Aprovação, Aprovada, Cancelada) +- Demandas por Categoria de Serviço +- Consumo Orçamento (Planejado x Comprometido x Realizado) +- Propostas por Fornecedor + +Os gráficos devem permitir: +- Filtro por período +- Filtro por Centro de Custo +- Filtro por Local + +#### 2.1.3 Alertas e Pendências +- Propostas aguardando leitura/validação +- Demandas paradas acima do SLA +- Propostas acima do orçamento (>20%) +- Aprovações pendentes do usuário logado + +Alertas devem ser destacados visualmente (cores/ícones). + +#### 2.1.4 Listas Operacionais (Quick Access) +- Minhas Demandas +- Demandas Críticas +- Propostas Pendentes +- Aprovações Pendentes + +Cada item deve permitir navegação direta para a demanda/proposta. + +#### 2.1.5 Personalização por Perfil +- Gestor de Facilities: foco operacional e fornecedores +- Financeiro/Diretoria: foco em orçamento, valores e riscos +- Solicitante: acompanhamento de status + +O sistema deve ocultar indicadores não relevantes ao perfil. + +## 3. Cadastros Base (Tabelas de Apoio) + +### 3.1 Locais / Unidades +- ID_Local +- Nome +- Endereço +- Centro de Custo +- Responsável pelo Centro de Custo + +**Regra:** Toda demanda deve estar vinculada a um Local e Centro de Custo. + +### 3.2 Categorias e Subcategorias de Serviço +- ID_Categoria +- Nome +- Subcategoria +- Criticidade Padrão +- SLA Padrão + +### 3.3 Fornecedores +- Tipo Pessoa (Física / Jurídica) +- CPF / CNPJ +- Categorias Atendidas +- Rating Facilities (1 a 5) +- Certidões Obrigatórias +- Status das Certidões + +**Regra:** Fornecedor com certidão vencida não pode receber OS. + +### 3.4 Orçamento Planejado +- Ano / Mês +- Centro de Custo +- Categoria +- Valor Planejado +- Valor Comprometido (calculado) +- Valor Realizado (calculado) + +### 3.5 Matriz de Aprovação (Alçadas) +- Centro de Custo +- Valor Mínimo +- Valor Máximo +- Perfil Aprovador +- Ordem Sequencial (se aplicável) + +## 4. Gestão de Demandas + +### 4.1 Abertura da Demanda +Campos principais: +- Título da Demanda +- Descrição +- Local +- Centro de Custo +- Categoria / Subcategoria +- Criticidade +- Data Desejada +- Upload de documentos (plantas, fotos, laudos) + +### 4.2 Definição de Escopo +- Criação de Itens de Linha obrigatórios + - Ex: Mão de Obra, Material, Equipamento +- Quantidade esperada (opcional) +- Observações técnicas + +**Validações:** +- Não permitir publicação sem itens de linha +- Não permitir publicação sem CC e Local + +## 5. Recebimento e Leitura de Propostas + +### 5.1 Upload de Propostas +- Upload de PDF pelo fornecedor +- Versionamento automático (V1, V2, V3…) + +### 5.2 OCR e Extração Inteligente +Campos extraídos: +- Valor Bruto +- Valor Líquido +- Impostos (ISS, INSS, PCC) +- Condição de Pagamento +- Prazo de Execução +- Data Estimada de Entrega + +O sistema deve: +- Mapear itens da proposta com os itens de linha do escopo +- Calcular Match de Escopo (%) +- Indicar nível de confiabilidade da extração + +## 6. Comparação e Equalização de Propostas + +### 6.1 Painel Comparativo +- Visualização em grid das propostas +- Comparação por item, valor total, impostos e prazo +- Destaque da proposta benchmark (menor valor) + +### 6.2 Deep Dive +- Clique em qualquer campo abre o trecho original do PDF +- Destaque visual da origem do dado + +### 6.3 Anotações +- Comentários privados (internos) +- Comentários públicos (questionamentos ao fornecedor) +- Registro com data, hora e usuário + +## 7. Controle Orçamentário +- Verificação automática de orçamento disponível +- Alertas de estouro (>20%) +- Bloqueio ou solicitação de revisão de escopo + +## 8. Workflow de Aprovação + +### 8.1 Regras Gerais +- Aprovação baseada no valor final da proposta selecionada +- Consulta automática à matriz de alçada + +### 8.2 Modelo Híbrido +- Até o limite da alçada → aprovação automática +- Acima do limite → fluxo sequencial: Facilities → Financeiro → Diretoria + +### 8.3 Exceções +- Demandas críticas/emergenciais +- Aprovação com ressalva (justificativa obrigatória) + +## 9. Ordem de Serviço (OS) +- Geração automática após aprovação +- Bloqueio se fornecedor estiver irregular +- Registro de valores comprometidos no orçamento + +## 10. Encerramento e Avaliação +- Confirmação da execução +- Avaliação do fornecedor +- Atualização do rating +- Consolidação do valor realizado + +## 11. Relatórios, Auditoria e Analytics +- Consumo orçamentário por CC e Categoria +- Saving gerado +- Histórico completo de decisões +- Lead time por status +- Gargalos de aprovação +- Exportação para PDF e Excel + +## 12. Requisitos Não Funcionais (Resumo) +- Controle de acesso por perfil +- Logs de auditoria +- Interface responsiva +- LGPD (tratamento de documentos e dados) diff --git a/docs/MANUAL-NEGOCIOS-v2.pdf b/docs/MANUAL-NEGOCIOS-v2.pdf new file mode 100644 index 0000000..cb4b442 Binary files /dev/null and b/docs/MANUAL-NEGOCIOS-v2.pdf differ diff --git a/docs/MANUAL-NEGOCIOS.html b/docs/MANUAL-NEGOCIOS.html new file mode 100644 index 0000000..5f7c702 --- /dev/null +++ b/docs/MANUAL-NEGOCIOS.html @@ -0,0 +1,1099 @@ + + + + + + HEFESTO - Manual de Negócios + + + + +
+
+
+
🔥
+
HEFESTO
+
+
Manual de Negócios
+
Visão Comercial e Estratégica
+
+
Kislanski Industries  |  AI Vertice
+
Fevereiro 2026  ·  v2.0
+
+
+ + +
+

Sumário

+
+ +
+ +
+ + + +
+

HEFESTO — Manual de Negócios

+

Sistema de Controle Orçamentário para Facilities

+

Versão 2.0 | Fevereiro 2025

+
+

1. Visão Geral do Produto

+

O HEFESTO é uma plataforma web para gestão integrada de Facilities, cobrindo todo o ciclo de vida de uma demanda de serviço — da abertura até a avaliação pós-execução — com controle orçamentário, workflow de aprovações e gestão de fornecedores.

+

O nome faz referência a Hefesto, o deus grego da forja e da construção, simbolizando a solidez e precisão que o sistema traz à gestão de instalações e infraestrutura.

+

2. Problema que Resolve

+

Cenário Atual (sem HEFESTO)

+

A gestão de Facilities em empresas de médio e grande porte sofre com:

+
    +
  • Demandas por e-mail e WhatsApp: Solicitações de manutenção, reformas e serviços chegam de forma desorganizada, sem rastreabilidade
  • +
  • Propostas em planilhas Excel: Comparação manual de cotações, sujeita a erros e vieses
  • +
  • Orçamento sem visibilidade: Gestores não sabem quanto já gastaram vs. quanto planejaram até consultar o financeiro
  • +
  • Aprovações por e-mail: Cadeia de aprovação informal, sem registro de quem aprovou o quê e quando
  • +
  • Fornecedores sem controle: Certidões vencidas, avaliações subjetivas, concentração em poucos prestadores
  • +
  • Zero rastreabilidade: Impossível saber o status de uma demanda sem ligar para alguém
  • +
+

Com o HEFESTO

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AntesDepois
Demanda por e-mailFormulário digital com SLA automático
Planilha de cotaçãoComparativo automático com equalização
Aprovação informalWorkflow com alçadas e registro completo
Orçamento no escuroDashboard em tempo real, planejado vs. realizado
Fornecedor sem controleCadastro com certidões, rating e histórico
Sem visibilidadePainel executivo com KPIs e alertas
+

3. Perfis de Usuário e Jornadas

+

3.1 Solicitante

+

Quem é: Gerente de unidade, coordenador, colaborador que identifica uma necessidade.

+

Jornada típica:
+1. Faz login no HEFESTO
+2. Abre nova demanda descrevendo o serviço necessário
+3. Seleciona local, categoria e prioridade
+4. Acompanha o status da demanda em tempo real
+5. Recebe notificações de cada mudança de status
+6. Avalia o serviço após conclusão

+

3.2 Gestor de Facilities

+

Quem é: Profissional responsável pela operação de Facilities.

+

Jornada típica:
+1. Visualiza dashboard com demandas pendentes e indicadores
+2. Analisa e complementa o escopo das demandas recebidas
+3. Seleciona fornecedores e envia pedidos de cotação
+4. Recebe e compara propostas (com auxílio de OCR e equalização)
+5. Seleciona a melhor proposta e encaminha para aprovação
+6. Após aprovação, emite Ordem de Serviço
+7. Acompanha execução e prazos
+8. Registra conclusão e solicita avaliação

+

3.3 Aprovador Financeiro

+

Quem é: Controller, gerente financeiro, analista de orçamento.

+

Jornada típica:
+1. Recebe notificação de demanda aguardando aprovação financeira
+2. Analisa valor vs. orçamento disponível no centro de custo
+3. Consulta comparativo de propostas e justificativa
+4. Aprova, rejeita ou devolve para correção
+5. Monitora orçamento planejado vs. realizado

+

3.4 Diretoria

+

Quem é: Diretor de operações, CFO, CEO.

+

Jornada típica:
+1. Aprova demandas de alto valor (acima da alçada do financeiro)
+2. Acompanha dashboard executivo com visão macro
+3. Analisa tendências de gastos e projeções

+

3.5 Fornecedor

+

Quem é: Prestador de serviços cadastrado no sistema.

+

Jornada típica:
+1. Recebe convite de cotação por e-mail
+2. Acessa o HEFESTO e submete proposta com valores e documentos
+3. Acompanha se foi selecionado
+4. Recebe Ordem de Serviço
+5. Registra conclusão do trabalho

+

3.6 Admin

+

Quem é: Administrador do sistema (TI ou gestor principal).

+

Jornada típica:
+1. Configura locais, centros de custo e categorias
+2. Gerencia usuários e perfis de acesso
+3. Define alçadas de aprovação
+4. Monitora audit log e saúde do sistema

+

4. Fluxo Completo de uma Demanda

+
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────────┐
+│ ABERTURA │───►│  ESCOPO  │───►│ COTAÇÃO  │───►│  PROPOSTAS   │
+│          │    │          │    │          │    │  RECEBIDAS   │
+└──────────┘    └──────────┘    └──────────┘    └──────┬───────┘
+Solicitante     Gestor Fac.     Gestor Fac.           │
+                                                       ▼
+┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────────┐
+│AVALIAÇÃO │◄───│CONCLUÍDA │◄───│EXECUÇÃO  │◄───│  APROVAÇÃO   │
+│          │    │          │    │          │    │  (Workflow)   │
+└──────────┘    └──────────┘    └──────────┘    └──────────────┘
+Solicitante     Gestor Fac.     Fornecedor      Financ./Diret.
+
+

Etapas Detalhadas

+
    +
  1. Abertura: Solicitante descreve a necessidade, seleciona local, categoria e prioridade
  2. +
  3. Escopo: Gestor de Facilities detalha os itens de linha, quantidades e especificações técnicas
  4. +
  5. Cotação: Gestor seleciona 3+ fornecedores e envia convite de cotação
  6. +
  7. Propostas Recebidas: Fornecedores submetem suas propostas com valores e documentos
  8. +
  9. Comparação: Sistema gera quadro comparativo automático com equalização de itens
  10. +
  11. Aprovação: Demanda segue workflow de alçadas conforme valor total
  12. +
  13. Ordem de Serviço: Após aprovação, OS é emitida automaticamente para o fornecedor selecionado
  14. +
  15. Execução: Fornecedor executa o serviço; gestor acompanha prazos e SLA
  16. +
  17. Avaliação: Solicitante e gestor avaliam qualidade, prazo e comunicação do fornecedor
  18. +
+

5. Funcionalidades por Módulo

+

5.1 Dashboard

+
    +
  • KPIs em tempo real (demandas abertas, em aprovação, gastos do mês)
  • +
  • Gráfico de gastos por local e categoria
  • +
  • Evolução mensal de gastos (planejado vs. realizado)
  • +
  • Ranking de fornecedores por nota
  • +
  • Indicadores de SLA (% dentro do prazo)
  • +
  • Alertas de certidões vencendo e orçamento estourando
  • +
+

5.2 Demandas

+
    +
  • Criação com formulário guiado (wizard)
  • +
  • Itens de linha com quantidade, unidade e valor estimado
  • +
  • Filtros por status, local, categoria, período e responsável
  • +
  • Timeline visual de cada demanda
  • +
  • Anexos e comentários
  • +
+

5.3 Propostas

+
    +
  • Submissão pelo fornecedor com upload de documentos
  • +
  • OCR automático: Extração de valores de propostas em PDF/imagem
  • +
  • Equalização automática: Normalização de itens para comparação justa
  • +
  • Quadro comparativo lado a lado
  • +
  • Seleção com justificativa obrigatória
  • +
+

5.4 Orçamento

+
    +
  • Planejamento anual por centro de custo e mês
  • +
  • Acompanhamento realizado vs. planejado em tempo real
  • +
  • Projeção de gastos com base no pipeline de demandas
  • +
  • Alertas automáticos ao atingir 80% e 100% do orçamento
  • +
  • Remanejamento entre centros de custo (com aprovação)
  • +
+

5.5 Fornecedores

+
    +
  • Cadastro completo (razão social, CNPJ, contato, especialidades)
  • +
  • Gestão de certidões com alerta de vencimento
  • +
  • Rating automático baseado em avaliações
  • +
  • Histórico de OS e valores contratados
  • +
  • Blacklist e restrições
  • +
+

5.6 Workflow

+
    +
  • Configuração de alçadas por faixa de valor
  • +
  • Aprovação em cadeia (sequencial) ou paralela
  • +
  • Notificações por e-mail e no sistema
  • +
  • Prazo para aprovação com escalação automática
  • +
  • Histórico completo de aprovações e rejeições
  • +
+

5.7 Ordens de Serviço

+
    +
  • Emissão automática após aprovação
  • +
  • Número sequencial por ano
  • +
  • Controle de datas (início, previsão, conclusão real)
  • +
  • Status: Emitida → Em Execução → Concluída → Avaliada
  • +
+

5.8 Relatórios

+
    +
  • Gastos por local, categoria e fornecedor
  • +
  • Economia gerada (valor estimado vs. contratado)
  • +
  • Tempo médio de ciclo por etapa
  • +
  • Exportação em PDF e Excel
  • +
+

5.9 ESG / Sustentabilidade 🌱

+

Funcionalidades inspiradas no SAP Sustainability Control Tower, entregues a uma fração do custo.

+
    +
  • Métricas ambientais: Registro e acompanhamento de consumo de energia (kWh), água (m³), resíduos (kg) e emissões de CO₂ (tCO₂e) por unidade
  • +
  • Dashboard ESG consolidado: Visão por unidade e período com gráficos de tendência
  • +
  • Metas ESG: Definição de metas ambientais com acompanhamento de progresso e status (em andamento, atingida, atrasada)
  • +
  • Relatórios para compliance: Dados prontos para relatórios de sustentabilidade corporativa e auditorias ambientais
  • +
  • Ideal para empresas do agronegócio que precisam demonstrar conformidade ESG a investidores e certificadoras
  • +
+

5.10 KPIs — Indicadores de Performance 📊

+
    +
  • Cálculo automático dos principais indicadores: % orçamento consumido, tempo médio de OS, rating médio de fornecedores, taxa de conclusão de demandas
  • +
  • Status semáforo (verde/amarelo/vermelho) para identificação visual imediata de desvios
  • +
  • Filtros por categoria, ano e centro de custo
  • +
  • Dashboard dedicado com visão gerencial consolidada
  • +
+

5.11 Auditoria e Compliance 🔒

+
    +
  • Trilha completa de auditoria: Registro detalhado de quem fez o quê, quando, com dados antes e depois
  • +
  • Relatório de conformidade por período para auditorias internas e externas
  • +
  • Exportação de logs em CSV e JSON
  • +
  • Compliance LGPD: Rastreabilidade total de acessos e alterações de dados pessoais
  • +
  • Atende requisitos de governança corporativa e normas ISO
  • +
+

5.12 Importação de Dados (Excel/CSV) 📥

+
    +
  • Upload de planilhas com validação automática de dados
  • +
  • Importação de orçamento planejado e demandas em massa
  • +
  • Relatório detalhado de erros por linha, facilitando correção
  • +
  • Ideal para migração de dados e carga inicial do sistema
  • +
+

5.13 Relatórios Automatizados 📈

+
    +
  • Orçamento mensal: Planejado vs. realizado com variações
  • +
  • Demandas por período: Status, valores e responsáveis
  • +
  • Ranking de fornecedores: Nota, volume e valor contratado
  • +
  • Performance de OS: SLA, tempo médio e taxa de conclusão
  • +
  • Exportação em JSON, CSV e PDF
  • +
+

5.14 Metas e Progresso 🎯

+
    +
  • Definição de metas por centro de custo: orçamentárias, operacionais e ESG
  • +
  • Acompanhamento de % atingido em tempo real
  • +
  • Status automático: em andamento, atingida, atrasada
  • +
  • Visão consolidada de progresso para toda a organização
  • +
+

5.15 Alertas Inteligentes 🔔

+
    +
  • Configuração de limites por centro de custo e categoria
  • +
  • Tipos de alerta: orçamento excedido, certidão vencendo, OS atrasada, meta em risco
  • +
  • Verificação automática com notificação proativa aos responsáveis
  • +
  • Prevenção de problemas antes que se tornem críticos
  • +
+

6. Modelo de Negócio

+

6.1 Formato

+

SaaS B2B (Software as a Service) com cobrança recorrente mensal.

+

6.2 Pricing

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlanoUnidadesUsuáriosPreço/mês
StarterAté 5Até 20R$ 1.500
ProfessionalAté 20Até 100R$ 4.500
EnterpriseIlimitadoIlimitadoSob consulta
+

Adicional por unidade: R$ 200/mês (plano Starter e Professional)
+Adicional por usuário: R$ 50/mês

+

6.3 Modelo de Receita

+
    +
  • Assinatura mensal recorrente (MRR)
  • +
  • Setup fee (implantação e treinamento): 2x o valor mensal
  • +
  • Customizações sob demanda (hora técnica)
  • +
+

6.4 Mercado-Alvo

+
    +
  • Empresas com 10+ unidades (shoppings, escritórios, fábricas, hospitais)
  • +
  • Áreas de Facilities / Manutenção / Infraestrutura
  • +
  • Empresas de administração de condomínios corporativos
  • +
  • Redes de varejo com muitas lojas
  • +
+

7. Diferenciais Competitivos

+

7.1 OCR em Propostas

+

Tecnologia de reconhecimento óptico para extrair valores de propostas enviadas em PDF ou imagem. Elimina a digitação manual e reduz erros.

+

7.2 Equalização Automática

+

Algoritmo que normaliza itens de diferentes propostas para comparação justa, mesmo quando fornecedores usam unidades, descrições ou agrupamentos diferentes.

+

7.3 Workflow de Alçadas

+

Motor de aprovação configurável por faixa de valor, com escalação automática, prazos e notificações. Garante conformidade e rastreabilidade total.

+

7.4 Orçamento em Tempo Real

+

Visão instantânea de planejado vs. realizado, com projeções baseadas no pipeline de demandas aprovadas e em andamento.

+

7.5 Rating de Fornecedores

+

Sistema de avaliação baseado em critérios objetivos (prazo, qualidade, comunicação) que alimenta o ranking automaticamente.

+

7.6 Audit Trail Completo

+

Toda ação no sistema é registrada com timestamp, usuário, IP e dados antes/depois. Compliance total para auditorias.

+

7.7 ESG Reporting — Inspirado no SAP Sustainability Control Tower

+

Capacidade de monitoramento ambiental (energia, água, resíduos, emissões CO₂) com dashboard consolidado e metas — funcionalidades equivalentes ao SAP Sustainability Control Tower, a uma fração do custo. Ideal para facilities management no agronegócio, onde a demonstração de práticas ESG é cada vez mais exigida por investidores, bancos e certificadoras.

+

7.8 KPIs com Semáforo Inteligente

+

Indicadores calculados automaticamente com classificação visual verde/amarelo/vermelho, eliminando a necessidade de análise manual de planilhas. Gestores identificam desvios instantaneamente.

+

7.9 Compliance LGPD e Rastreabilidade Total

+

Trilha de auditoria completa com exportação para auditorias externas. Atende requisitos da Lei Geral de Proteção de Dados (LGPD) com registro detalhado de todo acesso e manipulação de dados pessoais. Relatórios de conformidade prontos para ISO 27001 e auditorias corporativas.

+

7.10 Importação Inteligente de Dados

+

Upload de planilhas Excel/CSV com validação automática e relatório de erros — migração de dados sem dor de cabeça. Empresas que usam planilhas podem adotar o HEFESTO sem perder dados históricos.

+

7.11 Alertas Proativos

+

Sistema de alertas inteligentes que notifica responsáveis antes de problemas acontecerem: orçamento prestes a estourar, certidões vencendo, OS atrasadas e metas em risco. Gestão preventiva, não reativa.

+

7.12 HEFESTO vs. SAP — Comparativo

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FuncionalidadeSAPHEFESTO
Controle orçamentário
Workflow de aprovações
Dashboard ESG / Sustentabilidade✅ (Sustainability Control Tower)
KPIs com semáforo
Auditoria e compliance
Relatórios automatizados
Metas e progresso
Alertas inteligentes
OCR em propostas
Equalização automática
Foco em FacilitiesGenérico✅ Especializado
Tempo de implantação6–18 meses2–4 semanas
Custo de licençaAlto (6 dígitos/ano)Fração do custo
Ideal para agroComplexo demais✅ Perfeito
+

8. Roadmap de Evolução

+

Fase 1 — MVP (Atual) ✅

+
    +
  • [x] Autenticação e RBAC
  • +
  • [x] CRUD de demandas, fornecedores, propostas
  • +
  • [x] Workflow de aprovação com alçadas
  • +
  • [x] Dashboard com KPIs básicos
  • +
  • [x] Orçamento planejado vs. realizado
  • +
  • [x] Ordens de serviço
  • +
+

Fase 1.5 — Módulos SAP-Inspired (Fev 2025) ✅

+
    +
  • [x] ESG / Sustentabilidade (métricas ambientais e metas)
  • +
  • [x] KPIs com status semáforo
  • +
  • [x] Auditoria e Compliance avançado
  • +
  • [x] Importação de dados Excel/CSV
  • +
  • [x] Relatórios automatizados
  • +
  • [x] Metas e Progresso por centro de custo
  • +
  • [x] Alertas Inteligentes configuráveis
  • +
+

Fase 2 — Q2 2025

+
    +
  • [ ] OCR para extração de valores de propostas
  • +
  • [ ] App mobile (React Native) para aprovações rápidas
  • +
  • [ ] Integração com ERP (SAP, TOTVS) via API
  • +
  • [ ] Notificações push
  • +
+

Fase 3 — Q3 2025

+
    +
  • [ ] Módulo de contratos (gestão de contratos recorrentes)
  • +
  • [ ] Equalização automática de propostas com IA
  • +
  • [ ] Portal do fornecedor (self-service)
  • +
  • [ ] Multi-idioma (PT/EN/ES)
  • +
+

Fase 4 — Q4 2025

+
    +
  • [ ] BI avançado com drill-down
  • +
  • [ ] Predição de manutenção (ML)
  • +
  • [ ] Marketplace de fornecedores
  • +
  • [ ] White-label para revendas
  • +
+

9. Métricas de Sucesso

+

9.1 Métricas de Produto

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétricaMetaComo medir
Tempo médio de ciclo da demanda< 15 diasMédia de created_at até status CONCLUÍDA
% de demandas com 3+ propostas> 80%Demandas com 3+ propostas / total
% de aprovações no prazo> 95%Aprovações dentro do SLA configurado
Economia média por demanda> 12%(Valor estimado - valor contratado) / estimado
NPS do solicitante> 70Pesquisa trimestral
+

9.2 Métricas de Negócio

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétricaMeta Ano 1Meta Ano 2
MRR (Receita Mensal Recorrente)R$ 30.000R$ 150.000
Clientes ativos1040
Churn mensal< 3%< 2%
CAC (Custo de Aquisição)< R$ 5.000< R$ 4.000
LTV (Lifetime Value)> R$ 50.000> R$ 80.000
LTV/CAC> 10x> 20x
+

9.3 Métricas de Impacto no Cliente

+
    +
  • Redução de 70% no tempo gasto com gestão manual de cotações
  • +
  • Redução de 40% no ciclo de aprovação
  • +
  • Economia média de 15% em contratações por comparação efetiva
  • +
  • 100% de rastreabilidade em auditorias
  • +
  • Zero propostas perdidas ou esquecidas
  • +
+
+

Documento gerado automaticamente — HEFESTO v2.0

+
+ + + + \ No newline at end of file diff --git a/docs/MANUAL-NEGOCIOS.md b/docs/MANUAL-NEGOCIOS.md index 84d97e6..d91c6a3 100644 --- a/docs/MANUAL-NEGOCIOS.md +++ b/docs/MANUAL-NEGOCIOS.md @@ -2,7 +2,7 @@ **Sistema de Controle Orçamentário para Facilities** -Versão 1.0 | Fevereiro 2025 +Versão 2.0 | Fevereiro 2025 --- @@ -190,6 +190,53 @@ Solicitante Gestor Fac. Fornecedor Financ./Diret. - Tempo médio de ciclo por etapa - Exportação em PDF e Excel +### 5.9 ESG / Sustentabilidade 🌱 +Funcionalidades inspiradas no **SAP Sustainability Control Tower**, entregues a uma fração do custo. + +- **Métricas ambientais:** Registro e acompanhamento de consumo de energia (kWh), água (m³), resíduos (kg) e emissões de CO₂ (tCO₂e) por unidade +- **Dashboard ESG consolidado:** Visão por unidade e período com gráficos de tendência +- **Metas ESG:** Definição de metas ambientais com acompanhamento de progresso e status (em andamento, atingida, atrasada) +- **Relatórios para compliance:** Dados prontos para relatórios de sustentabilidade corporativa e auditorias ambientais +- Ideal para empresas do **agronegócio** que precisam demonstrar conformidade ESG a investidores e certificadoras + +### 5.10 KPIs — Indicadores de Performance 📊 +- **Cálculo automático** dos principais indicadores: % orçamento consumido, tempo médio de OS, rating médio de fornecedores, taxa de conclusão de demandas +- **Status semáforo** (verde/amarelo/vermelho) para identificação visual imediata de desvios +- Filtros por categoria, ano e centro de custo +- Dashboard dedicado com visão gerencial consolidada + +### 5.11 Auditoria e Compliance 🔒 +- **Trilha completa de auditoria:** Registro detalhado de quem fez o quê, quando, com dados antes e depois +- Relatório de conformidade por período para auditorias internas e externas +- Exportação de logs em CSV e JSON +- **Compliance LGPD:** Rastreabilidade total de acessos e alterações de dados pessoais +- Atende requisitos de governança corporativa e normas ISO + +### 5.12 Importação de Dados (Excel/CSV) 📥 +- Upload de planilhas com **validação automática** de dados +- Importação de orçamento planejado e demandas em massa +- Relatório detalhado de erros por linha, facilitando correção +- Ideal para migração de dados e carga inicial do sistema + +### 5.13 Relatórios Automatizados 📈 +- **Orçamento mensal:** Planejado vs. realizado com variações +- **Demandas por período:** Status, valores e responsáveis +- **Ranking de fornecedores:** Nota, volume e valor contratado +- **Performance de OS:** SLA, tempo médio e taxa de conclusão +- Exportação em JSON, CSV e PDF + +### 5.14 Metas e Progresso 🎯 +- Definição de metas por centro de custo: **orçamentárias**, **operacionais** e **ESG** +- Acompanhamento de % atingido em tempo real +- Status automático: em andamento, atingida, atrasada +- Visão consolidada de progresso para toda a organização + +### 5.15 Alertas Inteligentes 🔔 +- Configuração de limites por centro de custo e categoria +- **Tipos de alerta:** orçamento excedido, certidão vencendo, OS atrasada, meta em risco +- Verificação automática com notificação proativa aos responsáveis +- Prevenção de problemas antes que se tornem críticos + ## 6. Modelo de Negócio ### 6.1 Formato @@ -240,6 +287,40 @@ Sistema de avaliação baseado em critérios objetivos (prazo, qualidade, comuni ### 7.6 Audit Trail Completo Toda ação no sistema é registrada com timestamp, usuário, IP e dados antes/depois. Compliance total para auditorias. +### 7.7 ESG Reporting — Inspirado no SAP Sustainability Control Tower +Capacidade de monitoramento ambiental (energia, água, resíduos, emissões CO₂) com dashboard consolidado e metas — funcionalidades equivalentes ao SAP Sustainability Control Tower, a uma **fração do custo**. Ideal para facilities management no agronegócio, onde a demonstração de práticas ESG é cada vez mais exigida por investidores, bancos e certificadoras. + +### 7.8 KPIs com Semáforo Inteligente +Indicadores calculados automaticamente com classificação visual verde/amarelo/vermelho, eliminando a necessidade de análise manual de planilhas. Gestores identificam desvios instantaneamente. + +### 7.9 Compliance LGPD e Rastreabilidade Total +Trilha de auditoria completa com exportação para auditorias externas. Atende requisitos da Lei Geral de Proteção de Dados (LGPD) com registro detalhado de todo acesso e manipulação de dados pessoais. Relatórios de conformidade prontos para ISO 27001 e auditorias corporativas. + +### 7.10 Importação Inteligente de Dados +Upload de planilhas Excel/CSV com validação automática e relatório de erros — migração de dados sem dor de cabeça. Empresas que usam planilhas podem adotar o HEFESTO sem perder dados históricos. + +### 7.11 Alertas Proativos +Sistema de alertas inteligentes que notifica responsáveis **antes** de problemas acontecerem: orçamento prestes a estourar, certidões vencendo, OS atrasadas e metas em risco. Gestão preventiva, não reativa. + +### 7.12 HEFESTO vs. SAP — Comparativo + +| Funcionalidade | SAP | HEFESTO | +|---|---|---| +| Controle orçamentário | ✅ | ✅ | +| Workflow de aprovações | ✅ | ✅ | +| Dashboard ESG / Sustentabilidade | ✅ (Sustainability Control Tower) | ✅ | +| KPIs com semáforo | ✅ | ✅ | +| Auditoria e compliance | ✅ | ✅ | +| Relatórios automatizados | ✅ | ✅ | +| Metas e progresso | ✅ | ✅ | +| Alertas inteligentes | ✅ | ✅ | +| OCR em propostas | ❌ | ✅ | +| Equalização automática | ❌ | ✅ | +| Foco em Facilities | Genérico | ✅ Especializado | +| Tempo de implantação | 6–18 meses | 2–4 semanas | +| Custo de licença | Alto (6 dígitos/ano) | **Fração do custo** | +| Ideal para agro | Complexo demais | ✅ Perfeito | + ## 8. Roadmap de Evolução ### Fase 1 — MVP (Atual) ✅ @@ -250,6 +331,15 @@ Toda ação no sistema é registrada com timestamp, usuário, IP e dados antes/d - [x] Orçamento planejado vs. realizado - [x] Ordens de serviço +### Fase 1.5 — Módulos SAP-Inspired (Fev 2025) ✅ +- [x] ESG / Sustentabilidade (métricas ambientais e metas) +- [x] KPIs com status semáforo +- [x] Auditoria e Compliance avançado +- [x] Importação de dados Excel/CSV +- [x] Relatórios automatizados +- [x] Metas e Progresso por centro de custo +- [x] Alertas Inteligentes configuráveis + ### Fase 2 — Q2 2025 - [ ] OCR para extração de valores de propostas - [ ] App mobile (React Native) para aprovações rápidas @@ -301,4 +391,4 @@ Toda ação no sistema é registrada com timestamp, usuário, IP e dados antes/d --- -*Documento gerado automaticamente — HEFESTO v1.0* +*Documento gerado automaticamente — HEFESTO v2.0* diff --git a/docs/MANUAL-NEGOCIOS.pdf b/docs/MANUAL-NEGOCIOS.pdf index fddeaed..3c19830 100644 Binary files a/docs/MANUAL-NEGOCIOS.pdf and b/docs/MANUAL-NEGOCIOS.pdf differ diff --git a/docs/MANUAL-TECNICO-v2.pdf b/docs/MANUAL-TECNICO-v2.pdf new file mode 100644 index 0000000..9e14690 Binary files /dev/null and b/docs/MANUAL-TECNICO-v2.pdf differ diff --git a/docs/MANUAL-TECNICO.html b/docs/MANUAL-TECNICO.html new file mode 100644 index 0000000..554f0eb --- /dev/null +++ b/docs/MANUAL-TECNICO.html @@ -0,0 +1,2194 @@ + + + + + + HEFESTO - Manual Técnico + + + + +
+
+
+
🔥
+
HEFESTO
+
+
Manual Técnico
+
Documentação Técnica Completa
+
+
Kislanski Industries  |  AI Vertice
+
Fevereiro 2026  ·  v2.0
+
+
+ + + + + + +
+

HEFESTO — Manual Técnico

+

Sistema de Controle Orçamentário para Facilities

+

Versão 2.0 | Fevereiro 2025

+
+

1. Visão Geral da Arquitetura

+

O HEFESTO é uma aplicação web fullstack composta por:

+
    +
  • Backend: API REST em NestJS (Node.js) com TypeORM
  • +
  • Frontend: SPA em React + TypeScript com Vite
  • +
  • Banco de Dados: SQLite (desenvolvimento) / PostgreSQL (produção)
  • +
  • Autenticação: JWT com RBAC (Role-Based Access Control)
  • +
+
┌─────────────────┐     HTTP/REST     ┌─────────────────┐
+│   React SPA     │ ◄──────────────► │  NestJS API     │
+│   (Vite)        │     JWT Bearer    │  (TypeORM)      │
+│   Port 5173     │                   │  Port 3000      │
+└─────────────────┘                   └────────┬────────┘
+                                               │
+                                      ┌────────▼────────┐
+                                      │  SQLite / PG    │
+                                      └─────────────────┘
+
+

2. Stack Completa

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponenteTecnologiaVersão
RuntimeNode.js22.x LTS
Backend FrameworkNestJS10.x
ORMTypeORM0.3.x
Frontend FrameworkReact18.x
Build ToolVite5.x
LinguagemTypeScript5.x
UI ComponentsTailwind CSS3.x
BD DesenvolvimentoSQLite35.x
BD ProduçãoPostgreSQL16.x
Auth@nestjs/jwt + passport10.x
HTTP ClientAxios1.x
Validaçãoclass-validator0.14.x
Documentação API@nestjs/swagger7.x
+

3. Estrutura de Pastas

+

3.1 Backend

+
backend/
+├── src/
+│   ├── main.ts                    # Bootstrap da aplicação
+│   ├── app.module.ts              # Módulo raiz
+│   ├── common/                    # Guards, decorators, pipes, interceptors
+│   │   ├── guards/
+│   │   │   ├── jwt-auth.guard.ts
+│   │   │   └── roles.guard.ts
+│   │   ├── decorators/
+│   │   │   ├── roles.decorator.ts
+│   │   │   └── current-user.decorator.ts
+│   │   └── interceptors/
+│   │       └── audit.interceptor.ts
+│   ├── database/                  # Configuração TypeORM, migrations, seeds
+│   │   ├── database.module.ts
+│   │   ├── migrations/
+│   │   └── seeds/
+│   └── modules/
+│       ├── auth/                  # Autenticação JWT, login, refresh token
+│       │   ├── auth.controller.ts
+│       │   ├── auth.service.ts
+│       │   ├── auth.module.ts
+│       │   ├── strategies/
+│       │   │   └── jwt.strategy.ts
+│       │   └── dto/
+│       ├── users/                 # CRUD de usuários e perfis
+│       │   ├── users.controller.ts
+│       │   ├── users.service.ts
+│       │   ├── entities/
+│       │   │   ├── usuario.entity.ts
+│       │   │   └── perfil.entity.ts
+│       │   └── dto/
+│       ├── locais/                # Gestão de locais/unidades
+│       ├── centros-custo/         # Centros de custo vinculados a locais
+│       ├── categorias/            # Categorias de serviço
+│       ├── fornecedores/          # Cadastro de fornecedores e certidões
+│       ├── demandas/              # Abertura e gestão de demandas
+│       ├── propostas/             # Recebimento e comparação de propostas
+│       ├── orcamento/             # Orçamento planejado vs realizado
+│       ├── workflow/              # Máquina de estados de aprovação
+│       ├── dashboard/             # Indicadores e relatórios
+│       ├── ordens-servico/        # Emissão e acompanhamento de OS
+│       ├── esg/                   # Métricas ESG e sustentabilidade
+│       ├── kpis/                  # Indicadores de performance
+│       ├── audit/                 # Auditoria e compliance avançado
+│       ├── import/                # Importação de dados Excel/CSV
+│       ├── relatorios/            # Relatórios automatizados
+│       ├── metas/                 # Metas e acompanhamento de progresso
+│       └── alertas-inteligentes/  # Configuração e verificação de alertas
+├── test/
+├── nest-cli.json
+├── tsconfig.json
+└── package.json
+
+

3.2 Frontend

+
frontend/
+├── src/
+│   ├── main.tsx                   # Entry point
+│   ├── App.tsx                    # Router + Layout
+│   ├── assets/                    # Imagens, ícones
+│   ├── components/                # Componentes reutilizáveis
+│   │   ├── Layout/
+│   │   ├── Sidebar/
+│   │   ├── Header/
+│   │   ├── DataTable/
+│   │   ├── StatusBadge/
+│   │   └── Charts/
+│   ├── pages/
+│   │   ├── Login.tsx              # Tela de autenticação
+│   │   ├── Dashboard.tsx          # Painel de indicadores
+│   │   ├── Demandas.tsx           # Lista de demandas com filtros
+│   │   ├── Landing.tsx            # Página inicial / nova demanda
+│   │   ├── Fornecedores.tsx       # Gestão de fornecedores
+│   │   ├── Orcamentos.tsx         # Orçamento planejado vs realizado
+│   │   ├── OrdensServico.tsx      # Ordens de serviço
+│   │   ├── Relatorios.tsx         # Relatórios gerenciais
+│   │   ├── Usuarios.tsx           # Administração de usuários
+│   │   ├── ESG.tsx                # Dashboard ESG e métricas ambientais
+│   │   ├── KPIs.tsx               # Painel de indicadores de performance
+│   │   ├── Auditoria.tsx          # Logs de auditoria e compliance
+│   │   ├── Importacao.tsx         # Upload de planilhas Excel/CSV
+│   │   ├── Metas.tsx              # Metas e progresso por centro de custo
+│   │   └── Alertas.tsx            # Configuração de alertas inteligentes
+│   ├── services/                  # Axios clients e API calls
+│   │   └── api.ts
+│   ├── types/                     # Interfaces TypeScript
+│   └── index.css                  # Tailwind directives
+├── vite.config.ts
+├── tailwind.config.js
+├── tsconfig.json
+└── package.json
+
+

4. Modelo de Dados

+

4.1 Diagrama de Entidades

+

O sistema possui 21 entidades principais:

+
perfis ──< usuarios ──< demandas ──< itens_linha
+                │              │
+                │              ├──< propostas
+                │              │
+                │              ├──< workflow_aprovacao
+                │              │
+                │              └──< ordens_servico ──< avaliacoes
+                │
+                └──< audit_log
+
+locais ──< centros_custo ──< orcamento_planejado
+
+categorias ──< demandas
+
+fornecedores ──< certidoes
+fornecedores ──< propostas
+
+alertas (standalone)
+
+locais ──< esg_metricas
+locais ──< esg_metas
+
+centros_custo ──< kpis
+centros_custo ──< metas
+
+centros_custo ──< alertas_config
+categorias ──< alertas_config
+
+

4.2 Descrição das Entidades

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#EntidadeDescriçãoCampos Principais
1perfisPerfis de acesso (RBAC)id, nome, descricao, permissoes (JSON)
2usuariosUsuários do sistemaid, nome, email, senha_hash, perfil_id, ativo, ultimo_acesso
3locaisUnidades/edifíciosid, nome, endereco, cidade, estado, cnpj, ativo
4centros_custoCentros de custo por localid, codigo, descricao, local_id, ativo
5categoriasCategorias de serviçoid, nome, descricao, sla_dias, ativo
6fornecedoresCadastro de fornecedoresid, razao_social, cnpj, contato, email, telefone, ativo, rating
7certidoesCertidões de fornecedoresid, fornecedor_id, tipo, arquivo_url, validade, status
8orcamento_planejadoBudget por centro de custo/anoid, centro_custo_id, ano, mes, valor_planejado, valor_realizado
9demandasDemandas de serviçoid, titulo, descricao, local_id, categoria_id, solicitante_id, status, prioridade, valor_estimado, created_at
10itens_linhaItens detalhados da demandaid, demanda_id, descricao, quantidade, unidade, valor_unitario
11propostasPropostas de fornecedoresid, demanda_id, fornecedor_id, valor_total, arquivo_url, data_validade, status, observacoes
12workflow_aprovacaoEtapas de aprovaçãoid, demanda_id, etapa, aprovador_id, status, comentario, data_acao
13ordens_servicoOrdens de serviço emitidasid, demanda_id, proposta_id, numero_os, data_inicio, data_fim_prevista, data_fim_real, status
14avaliacoesAvaliação pós-execuçãoid, ordem_servico_id, avaliador_id, nota, comentario, created_at
15audit_logLog de auditoriaid, usuario_id, acao, entidade, entidade_id, dados_antes, dados_depois, ip, created_at
16alertasNotificações e alertasid, usuario_id, tipo, mensagem, lido, referencia_tipo, referencia_id, created_at
17esg_metricasMétricas ambientais ESGid, local_id, tipo (energia/agua/residuos/emissoes_co2), valor, unidade_medida, periodo, observacoes, created_at
18esg_metasMetas ESGid, local_id, tipo, descricao, valor_alvo, valor_atual, percentual_atingido, status, prazo, created_at
19kpisIndicadores de performance calculadosid, nome, valor, unidade, status_semaforo, centro_custo_id, periodo, calculated_at
20metasMetas por centro de custoid, centro_custo_id, tipo (orcamento/operacional/esg), descricao, valor_alvo, valor_atual, percentual_atingido, status, prazo, created_at
21alertas_configConfiguração de alertas inteligentesid, tipo, centro_custo_id, categoria_id, limite_percentual, notificar_usuarios (JSON), ativo, created_at
+

4.3 Perfis de Acesso (RBAC)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PerfilPermissões
AdminAcesso total ao sistema, gestão de usuários e configurações
Gestor FacilitiesCRUD de demandas, fornecedores, propostas, OS; aprovação nível 1
Aprovador FinanceiroAprovação nível 2 (financeiro), visualização de orçamento
DiretoriaAprovação nível 3 (alçada máxima), dashboard executivo
SolicitanteAbertura de demandas, acompanhamento do próprio pedido
FornecedorEnvio de propostas, acompanhamento de OS atribuídas
+

5. API — Endpoints

+

5.1 Autenticação (/api/auth)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
POST/api/auth/loginLogin com email/senha → JWT
POST/api/auth/refreshRenovar token
POST/api/auth/logoutInvalidar token
GET/api/auth/meDados do usuário logado
POST/api/auth/forgot-passwordSolicitar reset de senha
POST/api/auth/reset-passwordResetar senha com token
+

5.2 Usuários (/api/users)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/usersListar usuários (paginado)
GET/api/users/:idDetalhes do usuário
POST/api/usersCriar usuário
PATCH/api/users/:idAtualizar usuário
DELETE/api/users/:idDesativar usuário
GET/api/perfisListar perfis
+

5.3 Locais (/api/locais)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/locaisListar locais
GET/api/locais/:idDetalhes do local
POST/api/locaisCriar local
PATCH/api/locais/:idAtualizar local
DELETE/api/locais/:idDesativar local
+

5.4 Centros de Custo (/api/centros-custo)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/centros-custoListar centros de custo
GET/api/centros-custo/:idDetalhes
POST/api/centros-custoCriar centro de custo
PATCH/api/centros-custo/:idAtualizar
DELETE/api/centros-custo/:idDesativar
+

5.5 Categorias (/api/categorias)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/categoriasListar categorias
GET/api/categorias/:idDetalhes
POST/api/categoriasCriar categoria
PATCH/api/categorias/:idAtualizar
DELETE/api/categorias/:idDesativar
+

5.6 Fornecedores (/api/fornecedores)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/fornecedoresListar fornecedores
GET/api/fornecedores/:idDetalhes com certidões
POST/api/fornecedoresCadastrar fornecedor
PATCH/api/fornecedores/:idAtualizar fornecedor
DELETE/api/fornecedores/:idDesativar
POST/api/fornecedores/:id/certidoesUpload de certidão
GET/api/fornecedores/:id/certidoesListar certidões
DELETE/api/fornecedores/:id/certidoes/:certidaoIdRemover certidão
+

5.7 Demandas (/api/demandas)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/demandasListar demandas (filtros: status, local, categoria, período)
GET/api/demandas/:idDetalhes completos da demanda
POST/api/demandasCriar nova demanda
PATCH/api/demandas/:idAtualizar demanda
DELETE/api/demandas/:idCancelar demanda
POST/api/demandas/:id/itensAdicionar item de linha
PATCH/api/demandas/:id/itens/:itemIdAtualizar item
DELETE/api/demandas/:id/itens/:itemIdRemover item
POST/api/demandas/:id/enviar-cotacaoEnviar para cotação
GET/api/demandas/:id/comparativoComparativo de propostas
+

5.8 Propostas (/api/propostas)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/propostasListar propostas
GET/api/propostas/:idDetalhes da proposta
POST/api/propostasSubmeter proposta (fornecedor)
PATCH/api/propostas/:idAtualizar proposta
DELETE/api/propostas/:idRetirar proposta
POST/api/propostas/:id/aceitarAceitar proposta
POST/api/propostas/:id/rejeitarRejeitar proposta
POST/api/propostas/:id/ocrProcessar OCR do arquivo
+

5.9 Orçamento (/api/orcamento)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/orcamentoOrçamento geral (filtros: ano, local, centro_custo)
GET/api/orcamento/:idDetalhes da linha orçamentária
POST/api/orcamentoCriar linha orçamentária
PATCH/api/orcamento/:idAtualizar valores
GET/api/orcamento/resumoResumo planejado vs realizado
GET/api/orcamento/projecaoProjeção de gastos
+

5.10 Workflow (/api/workflow)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/workflow/pendentesAprovações pendentes do usuário logado
GET/api/workflow/demanda/:demandaIdHistórico de aprovações da demanda
POST/api/workflow/aprovarAprovar etapa
POST/api/workflow/rejeitarRejeitar etapa
POST/api/workflow/devolverDevolver para correção
GET/api/workflow/alçadasConsultar alçadas configuradas
+

5.11 Dashboard (/api/dashboard)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/dashboard/indicadoresKPIs gerais
GET/api/dashboard/demandas-por-statusDemandas agrupadas por status
GET/api/dashboard/gastos-por-localGastos por unidade
GET/api/dashboard/gastos-por-categoriaGastos por categoria
GET/api/dashboard/evolucao-mensalSérie temporal de gastos
GET/api/dashboard/top-fornecedoresRanking de fornecedores
GET/api/dashboard/slaIndicadores de SLA
+

5.12 Ordens de Serviço (/api/ordens-servico)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/ordens-servicoListar OS
GET/api/ordens-servico/:idDetalhes da OS
POST/api/ordens-servicoEmitir OS
PATCH/api/ordens-servico/:idAtualizar OS
POST/api/ordens-servico/:id/iniciarMarcar início da execução
POST/api/ordens-servico/:id/concluirMarcar conclusão
POST/api/ordens-servico/:id/avaliarAvaliar serviço prestado
+

5.13 Alertas (/api/alertas)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/alertasListar alertas do usuário
PATCH/api/alertas/:id/lidoMarcar como lido
DELETE/api/alertas/:idRemover alerta
+

5.14 ESG / Sustentabilidade (/api/esg)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/esg/metricasListar métricas ambientais (filtros: unidade, período, tipo)
POST/api/esg/metricasRegistrar métrica ambiental
PATCH/api/esg/metricas/:idAtualizar métrica
DELETE/api/esg/metricas/:idRemover métrica
GET/api/esg/dashboardDashboard consolidado ESG por unidade/período
GET/api/esg/metasListar metas ESG
POST/api/esg/metasCriar meta ESG
PATCH/api/esg/metas/:idAtualizar meta ESG
GET/api/esg/metas/:id/progressoProgresso da meta ESG
+

Exemplo — POST /api/esg/metricas:

+
// Request
+{
+  "local_id": 3,
+  "tipo": "energia",
+  "valor": 12500.50,
+  "unidade_medida": "kWh",
+  "periodo": "2025-06",
+  "observacoes": "Consumo sede administrativa"
+}
+// Response 201
+{
+  "id": 42,
+  "local_id": 3,
+  "tipo": "energia",
+  "valor": 12500.50,
+  "unidade_medida": "kWh",
+  "periodo": "2025-06",
+  "created_at": "2025-06-30T14:00:00Z"
+}
+
+

Tipos de métricas suportados: energia (kWh), agua (m³), residuos (kg), emissoes_co2 (tCO₂e).

+

5.15 KPIs — Indicadores de Performance (/api/kpis)

+ + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/kpisListar KPIs calculados (filtros: categoria, ano, centro_custo)
GET/api/kpis/dashboardDashboard de KPIs com status semáforo
+

KPIs calculados automaticamente:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KPIFórmulaVerdeAmareloVermelho
% Orçamento Consumidorealizado / planejado × 100≤ 80%81–100%> 100%
Tempo Médio de OSmédia(data_fim_real - data_inicio)≤ SLASLA+20%> SLA+20%
Rating Fornecedoresmédia das avaliações≥ 4.03.0–3.9< 3.0
Taxa Conclusão Demandasconcluídas / total × 100≥ 90%70–89%< 70%
+

Exemplo — GET /api/kpis/dashboard:

+
// Response 200
+{
+  "periodo": "2025-06",
+  "kpis": [
+    {
+      "nome": "orcamento_consumido",
+      "valor": 78.5,
+      "unidade": "%",
+      "status": "verde",
+      "centro_custo": "ADM-001"
+    },
+    {
+      "nome": "tempo_medio_os",
+      "valor": 12.3,
+      "unidade": "dias",
+      "status": "amarelo",
+      "centro_custo": "ADM-001"
+    }
+  ]
+}
+
+

5.16 Auditoria e Compliance (/api/audit)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/audit/logsTrilha de auditoria completa (filtros: usuario, entidade, período, ação)
GET/api/audit/compliance-reportRelatório de conformidade por período
GET/api/audit/exportExportação de logs em CSV ou JSON (?format=csv\|json)
+

Exemplo — GET /api/audit/logs?entidade=demandas&periodo_inicio=2025-06-01:

+
// Response 200
+{
+  "total": 245,
+  "page": 1,
+  "data": [
+    {
+      "id": 1023,
+      "usuario": "joao.silva@empresa.com",
+      "acao": "UPDATE",
+      "entidade": "demandas",
+      "entidade_id": 87,
+      "dados_antes": { "status": "EM_COTACAO" },
+      "dados_depois": { "status": "PROPOSTAS_RECEBIDAS" },
+      "ip": "192.168.1.50",
+      "created_at": "2025-06-15T10:32:00Z"
+    }
+  ]
+}
+
+

5.17 Importação de Dados (/api/import)

+ + + + + + + + + + + + + + + +
MétodoRotaDescrição
POST/api/import/excelUpload de planilha Excel/CSV com validação automática
+

Tipos de importação: orcamento, demandas.

+

Exemplo — POST /api/import/excel (multipart/form-data):

+
// Request: file=planilha.xlsx, tipo=orcamento
+// Response 200
+{
+  "status": "success",
+  "registros_importados": 48,
+  "registros_com_erro": 2,
+  "erros": [
+    { "linha": 15, "campo": "valor_planejado", "mensagem": "Valor inválido" },
+    { "linha": 32, "campo": "centro_custo_id", "mensagem": "Centro de custo não encontrado" }
+  ]
+}
+
+

5.18 Relatórios Automatizados (/api/relatorios)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/relatorios/orcamento-mensalRelatório de orçamento mensal (filtros: ano, mês, local)
GET/api/relatorios/demandas-periodoDemandas por período com status e valores
GET/api/relatorios/fornecedores-rankingRanking de fornecedores com nota, valor e volume
GET/api/relatorios/os-performancePerformance de OS (SLA, tempo médio, conclusão)
+

Todos os relatórios suportam ?format=json|csv|pdf.

+

Exemplo — GET /api/relatorios/fornecedores-ranking?ano=2025&limit=10:

+
// Response 200
+{
+  "periodo": "2025",
+  "ranking": [
+    {
+      "posicao": 1,
+      "fornecedor": "TechServ Ltda",
+      "rating": 4.8,
+      "total_os": 23,
+      "valor_total": 187500.00,
+      "sla_cumprido": 95.6
+    }
+  ]
+}
+
+

5.19 Metas e Progresso (/api/metas)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
GET/api/metasListar metas (filtros: centro_custo, tipo, status)
POST/api/metasCriar meta
PATCH/api/metas/:idAtualizar meta
DELETE/api/metas/:idRemover meta
GET/api/metas/progressoProgresso geral de todas as metas
GET/api/metas/:id/progressoProgresso de meta específica
+

Tipos de meta: orcamento, operacional, esg.

+

Status: em_andamento, atingida, atrasada.

+

Exemplo — POST /api/metas:

+
// Request
+{
+  "centro_custo_id": 5,
+  "tipo": "orcamento",
+  "descricao": "Reduzir gastos com manutenção em 10%",
+  "valor_alvo": 90,
+  "unidade": "%",
+  "prazo": "2025-12-31"
+}
+// Response 201
+{
+  "id": 12,
+  "centro_custo_id": 5,
+  "tipo": "orcamento",
+  "descricao": "Reduzir gastos com manutenção em 10%",
+  "valor_alvo": 90,
+  "valor_atual": 0,
+  "percentual_atingido": 0,
+  "status": "em_andamento",
+  "prazo": "2025-12-31",
+  "created_at": "2025-02-09T15:00:00Z"
+}
+
+

5.20 Alertas Inteligentes (/api/alertas)

+
+

Nota: Os endpoints abaixo complementam os alertas básicos da seção 5.13 com configuração avançada e verificação automática.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MétodoRotaDescrição
POST/api/alertas/configurarConfigurar regra de alerta inteligente
GET/api/alertas/configurarListar configurações de alertas
PATCH/api/alertas/configurar/:idAtualizar configuração
DELETE/api/alertas/configurar/:idRemover configuração
POST/api/alertas/verificarDisparar verificação manual de todos os alertas
+

Tipos de alerta: orcamento_excedido, certidao_vencendo, os_atrasada, meta_em_risco.

+

Exemplo — POST /api/alertas/configurar:

+
// Request
+{
+  "tipo": "orcamento_excedido",
+  "centro_custo_id": 5,
+  "limite_percentual": 85,
+  "notificar_usuarios": [1, 3, 7],
+  "ativo": true
+}
+// Response 201
+{
+  "id": 8,
+  "tipo": "orcamento_excedido",
+  "centro_custo_id": 5,
+  "limite_percentual": 85,
+  "notificar_usuarios": [1, 3, 7],
+  "ativo": true,
+  "created_at": "2025-02-09T15:00:00Z"
+}
+
+

Total: 95 endpoints

+

6. Autenticação e Autorização

+

6.1 Fluxo JWT

+
    +
  1. Usuário faz POST /api/auth/login com email e senha
  2. +
  3. Backend valida credenciais e retorna { accessToken, refreshToken }
  4. +
  5. Frontend armazena tokens e envia Authorization: Bearer <accessToken> em todas as requests
  6. +
  7. Token expira em 1h; refresh token expira em 7d
  8. +
  9. Frontend usa /api/auth/refresh para renovar automaticamente
  10. +
+

6.2 Guards NestJS

+
@UseGuards(JwtAuthGuard, RolesGuard)
+@Roles('admin', 'gestor_facilities')
+@Get('demandas')
+findAll() { ... }
+
+
    +
  • JwtAuthGuard: valida o token JWT
  • +
  • RolesGuard: verifica se o perfil do usuário está na lista de roles permitidas
  • +
  • @CurrentUser(): decorator customizado que injeta o usuário logado
  • +
+

7. Workflow de Aprovação

+

7.1 Máquina de Estados

+
RASCUNHO → EM_ESCOPO → EM_COTAÇÃO → PROPOSTAS_RECEBIDAS
+    → EM_COMPARAÇÃO → AGUARDANDO_APROVAÇÃO → APROVADA
+    → OS_EMITIDA → EM_EXECUÇÃO → CONCLUÍDA → AVALIADA
+
+Estados alternativos:
+    AGUARDANDO_APROVAÇÃO → REJEITADA
+    AGUARDANDO_APROVAÇÃO → DEVOLVIDA → EM_ESCOPO
+    Qualquer estado → CANCELADA
+
+

7.2 Alçadas de Aprovação

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Faixa de ValorAprovador
Até R$ 5.000Gestor Facilities
R$ 5.001 — R$ 50.000Gestor Facilities + Aprovador Financeiro
R$ 50.001 — R$ 200.000Gestor + Financeiro + Diretoria
Acima de R$ 200.000Gestor + Financeiro + Diretoria + CEO
+

Cada etapa gera um registro em workflow_aprovacao com timestamp, aprovador e comentário.

+

8. Como Rodar Localmente

+

8.1 Pré-requisitos

+
    +
  • Node.js 22.x
  • +
  • npm 10.x
  • +
  • Git
  • +
+

8.2 Backend

+
cd backend
+cp .env.example .env          # Ajustar variáveis
+npm install
+npm run build
+npm run migration:run         # Criar tabelas
+npm run seed                  # Dados iniciais
+npm start                     # Porta 3000
+
+

8.3 Frontend

+
cd frontend
+cp .env.example .env          # VITE_API_URL=http://localhost:3000/api
+npm install
+npm run dev                   # Porta 5173
+
+

8.4 Acesso Inicial

+
    +
  • Admin: admin@hefesto.com.br / admin123
  • +
  • Swagger: http://localhost:3000/api/docs
  • +
+

9. Deploy em Produção

+

9.1 Infraestrutura

+
    +
  • Servidor: DigitalOcean Droplet (Ubuntu 24.04, 2 vCPU, 4GB RAM)
  • +
  • Proxy reverso: Nginx
  • +
  • SSL: Let's Encrypt (Certbot)
  • +
  • Processo: PM2 (backend) / Nginx serve build estático (frontend)
  • +
  • BD: PostgreSQL 16
  • +
+

9.2 Nginx Config

+
server {
+    listen 443 ssl;
+    server_name hefesto.exemplo.com.br;
+
+    ssl_certificate /etc/letsencrypt/live/hefesto.exemplo.com.br/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/hefesto.exemplo.com.br/privkey.pem;
+
+    # Frontend (build estático)
+    location / {
+        root /var/www/hefesto/frontend/dist;
+        try_files $uri $uri/ /index.html;
+    }
+
+    # API Backend
+    location /api {
+        proxy_pass http://127.0.0.1:3000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+}
+
+

9.3 PM2

+
cd /var/www/hefesto/backend
+pm2 start dist/main.js --name hefesto-api
+pm2 save
+pm2 startup
+
+

10. Variáveis de Ambiente

+

Backend (.env)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariávelDescriçãoExemplo
NODE_ENVAmbienteproduction
PORTPorta da API3000
DB_TYPETipo de bancopostgres
DB_HOSTHost do bancolocalhost
DB_PORTPorta do banco5432
DB_USERNAMEUsuário do bancohefesto
DB_PASSWORDSenha do banco***
DB_DATABASENome do bancohefesto_prod
JWT_SECRETChave secreta JWTminha-chave-secreta-256bits
JWT_EXPIRATIONTempo de expiração do access token3600s
JWT_REFRESH_EXPIRATIONTempo de expiração do refresh token7d
CORS_ORIGINOrigem permitidahttps://hefesto.exemplo.com.br
OCR_API_KEYChave da API de OCR***
SMTP_HOSTServidor SMTPsmtp.gmail.com
SMTP_PORTPorta SMTP587
SMTP_USERUsuário SMTPnoreply@hefesto.com.br
SMTP_PASSSenha SMTP***
+

Frontend (.env)

+ + + + + + + + + + + + + + + + + + + + +
VariávelDescriçãoExemplo
VITE_API_URLURL base da APIhttps://hefesto.exemplo.com.br/api
VITE_APP_NAMENome da aplicaçãoHEFESTO
+
+

Documento gerado automaticamente — HEFESTO v2.0

+
+ + + + \ No newline at end of file diff --git a/docs/MANUAL-TECNICO.md b/docs/MANUAL-TECNICO.md index 65098c6..8878fa6 100644 --- a/docs/MANUAL-TECNICO.md +++ b/docs/MANUAL-TECNICO.md @@ -2,7 +2,7 @@ **Sistema de Controle Orçamentário para Facilities** -Versão 1.0 | Fevereiro 2025 +Versão 2.0 | Fevereiro 2025 --- @@ -91,7 +91,14 @@ backend/ │ ├── orcamento/ # Orçamento planejado vs realizado │ ├── workflow/ # Máquina de estados de aprovação │ ├── dashboard/ # Indicadores e relatórios -│ └── ordens-servico/ # Emissão e acompanhamento de OS +│ ├── ordens-servico/ # Emissão e acompanhamento de OS +│ ├── esg/ # Métricas ESG e sustentabilidade +│ ├── kpis/ # Indicadores de performance +│ ├── audit/ # Auditoria e compliance avançado +│ ├── import/ # Importação de dados Excel/CSV +│ ├── relatorios/ # Relatórios automatizados +│ ├── metas/ # Metas e acompanhamento de progresso +│ └── alertas-inteligentes/ # Configuração e verificação de alertas ├── test/ ├── nest-cli.json ├── tsconfig.json @@ -122,7 +129,13 @@ frontend/ │ │ ├── Orcamentos.tsx # Orçamento planejado vs realizado │ │ ├── OrdensServico.tsx # Ordens de serviço │ │ ├── Relatorios.tsx # Relatórios gerenciais -│ │ └── Usuarios.tsx # Administração de usuários +│ │ ├── Usuarios.tsx # Administração de usuários +│ │ ├── ESG.tsx # Dashboard ESG e métricas ambientais +│ │ ├── KPIs.tsx # Painel de indicadores de performance +│ │ ├── Auditoria.tsx # Logs de auditoria e compliance +│ │ ├── Importacao.tsx # Upload de planilhas Excel/CSV +│ │ ├── Metas.tsx # Metas e progresso por centro de custo +│ │ └── Alertas.tsx # Configuração de alertas inteligentes │ ├── services/ # Axios clients e API calls │ │ └── api.ts │ ├── types/ # Interfaces TypeScript @@ -137,7 +150,7 @@ frontend/ ### 4.1 Diagrama de Entidades -O sistema possui 16 entidades principais: +O sistema possui 21 entidades principais: ``` perfis ──< usuarios ──< demandas ──< itens_linha @@ -158,6 +171,15 @@ fornecedores ──< certidoes fornecedores ──< propostas alertas (standalone) + +locais ──< esg_metricas +locais ──< esg_metas + +centros_custo ──< kpis +centros_custo ──< metas + +centros_custo ──< alertas_config +categorias ──< alertas_config ``` ### 4.2 Descrição das Entidades @@ -180,6 +202,11 @@ alertas (standalone) | 14 | **avaliacoes** | Avaliação pós-execução | id, ordem_servico_id, avaliador_id, nota, comentario, created_at | | 15 | **audit_log** | Log de auditoria | id, usuario_id, acao, entidade, entidade_id, dados_antes, dados_depois, ip, created_at | | 16 | **alertas** | Notificações e alertas | id, usuario_id, tipo, mensagem, lido, referencia_tipo, referencia_id, created_at | +| 17 | **esg_metricas** | Métricas ambientais ESG | id, local_id, tipo (energia/agua/residuos/emissoes_co2), valor, unidade_medida, periodo, observacoes, created_at | +| 18 | **esg_metas** | Metas ESG | id, local_id, tipo, descricao, valor_alvo, valor_atual, percentual_atingido, status, prazo, created_at | +| 19 | **kpis** | Indicadores de performance calculados | id, nome, valor, unidade, status_semaforo, centro_custo_id, periodo, calculated_at | +| 20 | **metas** | Metas por centro de custo | id, centro_custo_id, tipo (orcamento/operacional/esg), descricao, valor_alvo, valor_atual, percentual_atingido, status, prazo, created_at | +| 21 | **alertas_config** | Configuração de alertas inteligentes | id, tipo, centro_custo_id, categoria_id, limite_percentual, notificar_usuarios (JSON), ativo, created_at | ### 4.3 Perfis de Acesso (RBAC) @@ -341,7 +368,245 @@ alertas (standalone) | PATCH | `/api/alertas/:id/lido` | Marcar como lido | | DELETE | `/api/alertas/:id` | Remover alerta | -**Total: 68 endpoints** +### 5.14 ESG / Sustentabilidade (`/api/esg`) + +| Método | Rota | Descrição | +|---|---|---| +| GET | `/api/esg/metricas` | Listar métricas ambientais (filtros: unidade, período, tipo) | +| POST | `/api/esg/metricas` | Registrar métrica ambiental | +| PATCH | `/api/esg/metricas/:id` | Atualizar métrica | +| DELETE | `/api/esg/metricas/:id` | Remover métrica | +| GET | `/api/esg/dashboard` | Dashboard consolidado ESG por unidade/período | +| GET | `/api/esg/metas` | Listar metas ESG | +| POST | `/api/esg/metas` | Criar meta ESG | +| PATCH | `/api/esg/metas/:id` | Atualizar meta ESG | +| GET | `/api/esg/metas/:id/progresso` | Progresso da meta ESG | + +**Exemplo — POST `/api/esg/metricas`:** +```json +// Request +{ + "local_id": 3, + "tipo": "energia", + "valor": 12500.50, + "unidade_medida": "kWh", + "periodo": "2025-06", + "observacoes": "Consumo sede administrativa" +} +// Response 201 +{ + "id": 42, + "local_id": 3, + "tipo": "energia", + "valor": 12500.50, + "unidade_medida": "kWh", + "periodo": "2025-06", + "created_at": "2025-06-30T14:00:00Z" +} +``` + +**Tipos de métricas suportados:** `energia` (kWh), `agua` (m³), `residuos` (kg), `emissoes_co2` (tCO₂e). + +### 5.15 KPIs — Indicadores de Performance (`/api/kpis`) + +| Método | Rota | Descrição | +|---|---|---| +| GET | `/api/kpis` | Listar KPIs calculados (filtros: categoria, ano, centro_custo) | +| GET | `/api/kpis/dashboard` | Dashboard de KPIs com status semáforo | + +**KPIs calculados automaticamente:** + +| KPI | Fórmula | Verde | Amarelo | Vermelho | +|---|---|---|---|---| +| % Orçamento Consumido | realizado / planejado × 100 | ≤ 80% | 81–100% | > 100% | +| Tempo Médio de OS | média(data_fim_real - data_inicio) | ≤ SLA | SLA+20% | > SLA+20% | +| Rating Fornecedores | média das avaliações | ≥ 4.0 | 3.0–3.9 | < 3.0 | +| Taxa Conclusão Demandas | concluídas / total × 100 | ≥ 90% | 70–89% | < 70% | + +**Exemplo — GET `/api/kpis/dashboard`:** +```json +// Response 200 +{ + "periodo": "2025-06", + "kpis": [ + { + "nome": "orcamento_consumido", + "valor": 78.5, + "unidade": "%", + "status": "verde", + "centro_custo": "ADM-001" + }, + { + "nome": "tempo_medio_os", + "valor": 12.3, + "unidade": "dias", + "status": "amarelo", + "centro_custo": "ADM-001" + } + ] +} +``` + +### 5.16 Auditoria e Compliance (`/api/audit`) + +| Método | Rota | Descrição | +|---|---|---| +| GET | `/api/audit/logs` | Trilha de auditoria completa (filtros: usuario, entidade, período, ação) | +| GET | `/api/audit/compliance-report` | Relatório de conformidade por período | +| GET | `/api/audit/export` | Exportação de logs em CSV ou JSON (`?format=csv\|json`) | + +**Exemplo — GET `/api/audit/logs?entidade=demandas&periodo_inicio=2025-06-01`:** +```json +// Response 200 +{ + "total": 245, + "page": 1, + "data": [ + { + "id": 1023, + "usuario": "joao.silva@empresa.com", + "acao": "UPDATE", + "entidade": "demandas", + "entidade_id": 87, + "dados_antes": { "status": "EM_COTACAO" }, + "dados_depois": { "status": "PROPOSTAS_RECEBIDAS" }, + "ip": "192.168.1.50", + "created_at": "2025-06-15T10:32:00Z" + } + ] +} +``` + +### 5.17 Importação de Dados (`/api/import`) + +| Método | Rota | Descrição | +|---|---|---| +| POST | `/api/import/excel` | Upload de planilha Excel/CSV com validação automática | + +**Tipos de importação:** `orcamento`, `demandas`. + +**Exemplo — POST `/api/import/excel` (multipart/form-data):** +```json +// Request: file=planilha.xlsx, tipo=orcamento +// Response 200 +{ + "status": "success", + "registros_importados": 48, + "registros_com_erro": 2, + "erros": [ + { "linha": 15, "campo": "valor_planejado", "mensagem": "Valor inválido" }, + { "linha": 32, "campo": "centro_custo_id", "mensagem": "Centro de custo não encontrado" } + ] +} +``` + +### 5.18 Relatórios Automatizados (`/api/relatorios`) + +| Método | Rota | Descrição | +|---|---|---| +| GET | `/api/relatorios/orcamento-mensal` | Relatório de orçamento mensal (filtros: ano, mês, local) | +| GET | `/api/relatorios/demandas-periodo` | Demandas por período com status e valores | +| GET | `/api/relatorios/fornecedores-ranking` | Ranking de fornecedores com nota, valor e volume | +| GET | `/api/relatorios/os-performance` | Performance de OS (SLA, tempo médio, conclusão) | + +**Todos os relatórios suportam `?format=json|csv|pdf`.** + +**Exemplo — GET `/api/relatorios/fornecedores-ranking?ano=2025&limit=10`:** +```json +// Response 200 +{ + "periodo": "2025", + "ranking": [ + { + "posicao": 1, + "fornecedor": "TechServ Ltda", + "rating": 4.8, + "total_os": 23, + "valor_total": 187500.00, + "sla_cumprido": 95.6 + } + ] +} +``` + +### 5.19 Metas e Progresso (`/api/metas`) + +| Método | Rota | Descrição | +|---|---|---| +| GET | `/api/metas` | Listar metas (filtros: centro_custo, tipo, status) | +| POST | `/api/metas` | Criar meta | +| PATCH | `/api/metas/:id` | Atualizar meta | +| DELETE | `/api/metas/:id` | Remover meta | +| GET | `/api/metas/progresso` | Progresso geral de todas as metas | +| GET | `/api/metas/:id/progresso` | Progresso de meta específica | + +**Tipos de meta:** `orcamento`, `operacional`, `esg`. + +**Status:** `em_andamento`, `atingida`, `atrasada`. + +**Exemplo — POST `/api/metas`:** +```json +// Request +{ + "centro_custo_id": 5, + "tipo": "orcamento", + "descricao": "Reduzir gastos com manutenção em 10%", + "valor_alvo": 90, + "unidade": "%", + "prazo": "2025-12-31" +} +// Response 201 +{ + "id": 12, + "centro_custo_id": 5, + "tipo": "orcamento", + "descricao": "Reduzir gastos com manutenção em 10%", + "valor_alvo": 90, + "valor_atual": 0, + "percentual_atingido": 0, + "status": "em_andamento", + "prazo": "2025-12-31", + "created_at": "2025-02-09T15:00:00Z" +} +``` + +### 5.20 Alertas Inteligentes (`/api/alertas`) + +> **Nota:** Os endpoints abaixo complementam os alertas básicos da seção 5.13 com configuração avançada e verificação automática. + +| Método | Rota | Descrição | +|---|---|---| +| POST | `/api/alertas/configurar` | Configurar regra de alerta inteligente | +| GET | `/api/alertas/configurar` | Listar configurações de alertas | +| PATCH | `/api/alertas/configurar/:id` | Atualizar configuração | +| DELETE | `/api/alertas/configurar/:id` | Remover configuração | +| POST | `/api/alertas/verificar` | Disparar verificação manual de todos os alertas | + +**Tipos de alerta:** `orcamento_excedido`, `certidao_vencendo`, `os_atrasada`, `meta_em_risco`. + +**Exemplo — POST `/api/alertas/configurar`:** +```json +// Request +{ + "tipo": "orcamento_excedido", + "centro_custo_id": 5, + "limite_percentual": 85, + "notificar_usuarios": [1, 3, 7], + "ativo": true +} +// Response 201 +{ + "id": 8, + "tipo": "orcamento_excedido", + "centro_custo_id": 5, + "limite_percentual": 85, + "notificar_usuarios": [1, 3, 7], + "ativo": true, + "created_at": "2025-02-09T15:00:00Z" +} +``` + +**Total: 95 endpoints** ## 6. Autenticação e Autorização @@ -505,4 +770,4 @@ pm2 startup --- -*Documento gerado automaticamente — HEFESTO v1.0* +*Documento gerado automaticamente — HEFESTO v2.0* diff --git a/docs/MANUAL-TECNICO.pdf b/docs/MANUAL-TECNICO.pdf index eb2ea10..f832381 100644 Binary files a/docs/MANUAL-TECNICO.pdf and b/docs/MANUAL-TECNICO.pdf differ diff --git a/docs/generate-pdfs.py b/docs/generate-pdfs.py new file mode 100644 index 0000000..0d8a792 --- /dev/null +++ b/docs/generate-pdfs.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python3 +"""Generate beautiful PDF manuals for HEFESTO from Markdown sources.""" + +import markdown +import subprocess +import os +import tempfile + +MANUALS = [ + { + "md": "MANUAL-TECNICO.md", + "pdf": "MANUAL-TECNICO-v2.pdf", + "title": "Manual Técnico", + "subtitle": "Documentação Técnica Completa", + "version": "v2.0", + "type": "technical" + }, + { + "md": "MANUAL-NEGOCIOS.md", + "pdf": "MANUAL-NEGOCIOS-v2.pdf", + "title": "Manual de Negócios", + "subtitle": "Visão Comercial e Estratégica", + "version": "v2.0", + "type": "business" + } +] + +CSS = """ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); + +@page { + size: A4; + margin: 2cm 2cm 2.5cm 2cm; + @top-center { + content: ""; + } + @bottom-center { + content: counter(page); + font-family: 'Inter', 'Segoe UI', sans-serif; + font-size: 9pt; + color: #999; + } +} + +@page :first { + margin: 0; + @bottom-center { content: ""; } +} + +* { box-sizing: border-box; } + +body { + font-family: 'Inter', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; + font-size: 10.5pt; + line-height: 1.7; + color: #333; + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; +} + +/* COVER PAGE */ +.cover { + page-break-after: always; + width: 210mm; + height: 297mm; + margin: -2cm; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: linear-gradient(160deg, #0D1B2A 0%, #1A237E 40%, #1A237E 60%, #0D1B2A 100%); + color: white; + text-align: center; + position: relative; + overflow: hidden; +} + +.cover::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 8px; + background: linear-gradient(90deg, #E65100, #FF8F00, #FFB300); +} + +.cover::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 8px; + background: linear-gradient(90deg, #FFB300, #FF8F00, #E65100); +} + +.cover .logo-icon { + font-size: 80pt; + margin-bottom: 10px; + filter: drop-shadow(0 4px 20px rgba(255, 143, 0, 0.5)); +} + +.cover .brand { + font-size: 42pt; + font-weight: 900; + letter-spacing: 12px; + margin-bottom: 5px; + background: linear-gradient(90deg, #FF8F00, #FFB300); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.cover .divider { + width: 120px; + height: 3px; + background: linear-gradient(90deg, transparent, #FF8F00, transparent); + margin: 20px auto; +} + +.cover .manual-title { + font-size: 22pt; + font-weight: 300; + letter-spacing: 3px; + text-transform: uppercase; + color: rgba(255,255,255,0.95); + margin-bottom: 8px; +} + +.cover .manual-subtitle { + font-size: 12pt; + font-weight: 300; + color: rgba(255,255,255,0.6); + margin-bottom: 60px; +} + +.cover .meta { + position: absolute; + bottom: 50px; + text-align: center; + width: 100%; +} + +.cover .company { + font-size: 11pt; + font-weight: 500; + letter-spacing: 4px; + text-transform: uppercase; + color: rgba(255,255,255,0.5); + margin-bottom: 8px; +} + +.cover .date { + font-size: 10pt; + color: rgba(255,255,255,0.35); + letter-spacing: 2px; +} + +/* GEOMETRIC DECORATIONS */ +.cover .geo1 { + position: absolute; + top: 60px; + right: 60px; + width: 200px; + height: 200px; + border: 1px solid rgba(255,143,0,0.15); + border-radius: 50%; +} + +.cover .geo2 { + position: absolute; + bottom: 120px; + left: 40px; + width: 150px; + height: 150px; + border: 1px solid rgba(255,143,0,0.1); + transform: rotate(45deg); +} + +/* PAGE HEADER */ +.page-header { + page-break-after: avoid; + margin-bottom: 30px; + padding-bottom: 12px; + border-bottom: 2px solid #E65100; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 9pt; + color: #999; + letter-spacing: 2px; + text-transform: uppercase; +} + +.page-header .left { + font-weight: 700; + color: #1A237E; +} + +.page-header .right { + color: #E65100; +} + +/* TABLE OF CONTENTS */ +.toc-page { + page-break-after: always; +} + +.toc-page h2 { + font-size: 18pt; + color: #1A237E; + border: none; + padding: 0; + margin-bottom: 25px; + letter-spacing: 3px; + text-transform: uppercase; +} + +.toc-page h2::before { + content: ''; + display: block; + width: 50px; + height: 3px; + background: #E65100; + margin-bottom: 15px; +} + +/* HEADINGS */ +h1 { + font-size: 22pt; + font-weight: 800; + color: #1A237E; + margin-top: 40px; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 3px solid #E65100; + page-break-after: avoid; + letter-spacing: -0.5px; +} + +h2 { + font-size: 16pt; + font-weight: 700; + color: #1A237E; + margin-top: 35px; + margin-bottom: 15px; + padding-left: 15px; + border-left: 4px solid #E65100; + page-break-after: avoid; +} + +h3 { + font-size: 13pt; + font-weight: 600; + color: #283593; + margin-top: 25px; + margin-bottom: 12px; + page-break-after: avoid; +} + +h4 { + font-size: 11pt; + font-weight: 600; + color: #E65100; + margin-top: 20px; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* FIRST H1 — remove top margin after cover */ +.content > h1:first-child { + margin-top: 0; +} + +/* PARAGRAPHS */ +p { + margin-bottom: 12px; + text-align: justify; + hyphens: auto; +} + +/* STRONG/BOLD in special contexts */ +strong { + color: #1A237E; + font-weight: 600; +} + +/* TABLES */ +table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; + font-size: 9.5pt; + page-break-inside: avoid; + box-shadow: 0 1px 4px rgba(0,0,0,0.08); + border-radius: 6px; + overflow: hidden; +} + +thead { + background: linear-gradient(135deg, #1A237E, #283593); +} + +th { + color: white; + font-weight: 600; + text-align: left; + padding: 12px 14px; + font-size: 9pt; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +td { + padding: 10px 14px; + border-bottom: 1px solid #E8EAF6; + vertical-align: top; +} + +tbody tr:nth-child(even) { + background-color: #F5F5FF; +} + +tbody tr:hover { + background-color: #E8EAF6; +} + +/* Checkmark styling for comparison tables */ +td:has(text("✅")), td:has(text("❌")) { + text-align: center; + font-size: 14pt; +} + +/* CODE BLOCKS */ +pre { + background: #1E1E2E; + color: #CDD6F4; + border-radius: 8px; + padding: 18px 20px; + font-size: 9pt; + line-height: 1.6; + overflow-x: auto; + margin: 18px 0; + page-break-inside: avoid; + border-left: 4px solid #E65100; +} + +code { + font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + font-size: 9pt; +} + +p code, li code, td code { + background: #EDE7F6; + color: #4A148C; + padding: 2px 7px; + border-radius: 4px; + font-size: 8.5pt; +} + +/* LISTS */ +ul, ol { + margin: 10px 0; + padding-left: 24px; +} + +li { + margin-bottom: 6px; + line-height: 1.6; +} + +li::marker { + color: #E65100; + font-weight: bold; +} + +/* BLOCKQUOTES / CALLOUTS */ +blockquote { + background: linear-gradient(135deg, #FFF3E0, #FFF8E1); + border-left: 4px solid #FF8F00; + margin: 20px 0; + padding: 16px 20px; + border-radius: 0 8px 8px 0; + font-style: normal; + page-break-inside: avoid; +} + +blockquote p { + margin: 0; + color: #5D4037; +} + +blockquote strong { + color: #E65100; +} + +/* HORIZONTAL RULES */ +hr { + border: none; + height: 2px; + background: linear-gradient(90deg, #E65100, #FF8F00, transparent); + margin: 35px 0; +} + +/* LINKS */ +a { + color: #1A237E; + text-decoration: none; + border-bottom: 1px solid #E65100; +} + +/* INFO BOX */ +.info-box { + background: #E3F2FD; + border-left: 4px solid #1565C0; + padding: 14px 18px; + border-radius: 0 8px 8px 0; + margin: 18px 0; +} + +/* SUCCESS BOX */ +.success-box { + background: #E8F5E9; + border-left: 4px solid #2E7D32; + padding: 14px 18px; + border-radius: 0 8px 8px 0; + margin: 18px 0; +} + +/* WARNING BOX */ +.warning-box { + background: #FFF3E0; + border-left: 4px solid #E65100; + padding: 14px 18px; + border-radius: 0 8px 8px 0; + margin: 18px 0; +} + +/* PAGE BREAKS for major sections */ +h1 { + page-break-before: always; +} + +h1:first-of-type { + page-break-before: avoid; +} + +/* FOOTER NOTE */ +.doc-footer { + margin-top: 40px; + padding-top: 15px; + border-top: 1px solid #E0E0E0; + text-align: center; + font-size: 8.5pt; + color: #999; + font-style: italic; +} + +/* Emoji sizing */ +.emoji-icon { + font-size: 14pt; +} + +/* Print optimizations */ +@media print { + body { -webkit-print-color-adjust: exact !important; } + .cover { page-break-after: always; } + h1, h2, h3 { page-break-after: avoid; } + table, pre, blockquote { page-break-inside: avoid; } +} +""" + +COVER_HTML = """ +
+
+
+
🔥
+
HEFESTO
+
+
{title}
+
{subtitle}
+
+
Kislanski Industries  |  AI Vertice
+
Fevereiro 2026  ·  {version}
+
+
+""" + +def generate_html(md_content, manual_info): + extensions = ['tables', 'fenced_code', 'toc', 'nl2br', 'sane_lists'] + ext_configs = { + 'toc': {'title': '', 'toc_depth': '1-3'} + } + + md = markdown.Markdown(extensions=extensions, extension_configs=ext_configs) + html_body = md.convert(md_content) + toc_html = md.toc + + cover = COVER_HTML.format( + title=manual_info['title'], + subtitle=manual_info['subtitle'], + version=manual_info['version'] + ) + + header_title = f"HEFESTO — {manual_info['title']}" + + full_html = f""" + + + + + HEFESTO - {manual_info['title']} + + + + {cover} + +
+

Sumário

+ {toc_html} +
+ + + +
+ {html_body} +
+ + + +""" + + return full_html + + +def generate_pdf(html_path, pdf_path): + cmd = [ + 'google-chrome', + '--headless', + '--disable-gpu', + '--no-sandbox', + '--disable-software-rasterizer', + f'--print-to-pdf={pdf_path}', + '--print-to-pdf-no-header', + '--no-margins', + f'file://{html_path}' + ] + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode != 0: + # Try chromium as fallback + cmd[0] = 'chromium-browser' + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + return os.path.exists(pdf_path) + + +def main(): + docs_dir = os.path.dirname(os.path.abspath(__file__)) + + for manual in MANUALS: + md_path = os.path.join(docs_dir, manual['md']) + pdf_path = os.path.join(docs_dir, manual['pdf']) + + print(f"\n{'='*60}") + print(f"Generating: {manual['title']}") + print(f"{'='*60}") + + # Read markdown + with open(md_path, 'r', encoding='utf-8') as f: + md_content = f.read() + + # Generate HTML + html_content = generate_html(md_content, manual) + + # Write temp HTML + html_path = os.path.join(docs_dir, manual['md'].replace('.md', '.html')) + with open(html_path, 'w', encoding='utf-8') as f: + f.write(html_content) + print(f" ✓ HTML generated: {html_path}") + + # Generate PDF + if generate_pdf(html_path, pdf_path): + size_mb = os.path.getsize(pdf_path) / (1024*1024) + print(f" ✓ PDF generated: {pdf_path} ({size_mb:.1f} MB)") + else: + print(f" ✗ PDF generation failed!") + + # Cleanup HTML + # os.remove(html_path) + + print(f"\n{'='*60}") + print("Done!") + + +if __name__ == '__main__': + main() diff --git a/frontend/index.html b/frontend/index.html index db43c33..79b7415 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,8 +4,8 @@ - - HEFESTO - Controle Orçamentário + + Nexus Facilities diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9d7bd66..216ec1b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,14 @@ import Orcamentos from './pages/Orcamentos' import OrdensServico from './pages/OrdensServico' import Fornecedores from './pages/Fornecedores' import Relatorios from './pages/Relatorios' +import ESG from './pages/ESG' +import KPIs from './pages/KPIs' +import Auditoria from './pages/Auditoria' +import Importacao from './pages/Importacao' +import Metas from './pages/Metas' +import AlertasConfig from './pages/AlertasConfig' import Usuarios from './pages/Usuarios' +import Configuracao from './pages/Configuracao' interface PrivateRouteProps { children: React.ReactNode; @@ -31,6 +38,13 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index c71ccfc..a61cb97 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -14,7 +14,12 @@ import { Flame, ChevronLeft, Bell, - Search + Search, + Leaf, + Shield, + Upload, + Target, + Settings } from 'lucide-react' import { User } from '../types' @@ -32,6 +37,13 @@ const navItems: NavItem[] = [ { path: '/app/ordens-servico', label: 'Ordens de Serviço', icon: }, { path: '/app/fornecedores', label: 'Fornecedores', icon: }, { path: '/app/relatorios', label: 'Relatórios', icon: }, + { path: '/app/esg', label: 'ESG', icon: }, + { path: '/app/kpis', label: 'KPIs', icon: }, + { path: '/app/metas', label: 'Metas', icon: }, + { path: '/app/auditoria', label: 'Auditoria', icon: }, + { path: '/app/importacao', label: 'Importação', icon: }, + { path: '/app/alertas-config', label: 'Alertas', icon: }, + { path: '/app/configuracao', label: 'Configuração', icon: }, { path: '/app/usuarios', label: 'Usuários', icon: , adminOnly: true }, ] @@ -74,7 +86,7 @@ export default function Layout() { {sidebarOpen && ( - HEFESTO + Nexus Facilities )} + +
+ {children} +
+ {onSubmit && ( +
+ + +
+ )} + + + ) +} diff --git a/frontend/src/hooks/useLookups.ts b/frontend/src/hooks/useLookups.ts new file mode 100644 index 0000000..d847b52 --- /dev/null +++ b/frontend/src/hooks/useLookups.ts @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react' +import api from '../services/api' + +export type LookupMap = Record + +interface Lookups { + categoriaMap: LookupMap + centrosCustoMap: LookupMap + locaisMap: LookupMap + fornecedoresMap: LookupMap + categorias: { id: string; nome: string }[] + centrosCusto: { id: string; nome: string }[] + locais: { id: string; nome: string }[] + fornecedores: { id: string; nome: string }[] + loading: boolean +} + +function toMap(items: any[], nameField: string = 'nome'): LookupMap { + const map: LookupMap = {} + for (const item of items) { + map[item.id] = item[nameField] || item.razao_social || item.nome_fantasia || item.id + } + return map +} + +export function useLookups(): Lookups { + const [categoriaMap, setCategoriaMap] = useState({}) + const [centrosCustoMap, setCentrosCustoMap] = useState({}) + const [locaisMap, setLocaisMap] = useState({}) + const [fornecedoresMap, setFornecedoresMap] = useState({}) + const [categorias, setCategorias] = useState<{ id: string; nome: string }[]>([]) + const [centrosCusto, setCentrosCusto] = useState<{ id: string; nome: string }[]>([]) + const [locais, setLocais] = useState<{ id: string; nome: string }[]>([]) + const [fornecedores, setFornecedores] = useState<{ id: string; nome: string }[]>([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + Promise.all([ + api.get('/categorias').catch(() => ({ data: [] })), + api.get('/centros-custo').catch(() => ({ data: [] })), + api.get('/locais').catch(() => ({ data: [] })), + api.get('/fornecedores').catch(() => ({ data: [] })), + ]).then(([catRes, ccRes, locRes, fornRes]) => { + const cats = catRes.data || [] + const ccs = ccRes.data || [] + const locs = locRes.data || [] + const forns = fornRes.data || [] + + setCategorias(cats.map((c: any) => ({ id: c.id, nome: c.nome }))) + setCentrosCusto(ccs.map((c: any) => ({ id: c.id, nome: c.nome }))) + setLocais(locs.map((l: any) => ({ id: l.id, nome: l.nome }))) + setFornecedores(forns.map((f: any) => ({ id: f.id, nome: f.razao_social || f.nome_fantasia || f.nome }))) + + setCategoriaMap(toMap(cats)) + setCentrosCustoMap(toMap(ccs)) + setLocaisMap(toMap(locs)) + setFornecedoresMap(toMap(forns, 'razao_social')) + }).finally(() => setLoading(false)) + }, []) + + return { categoriaMap, centrosCustoMap, locaisMap, fornecedoresMap, categorias, centrosCusto, locais, fornecedores, loading } +} diff --git a/frontend/src/pages/AlertasConfig.tsx b/frontend/src/pages/AlertasConfig.tsx new file mode 100644 index 0000000..6e5ab71 --- /dev/null +++ b/frontend/src/pages/AlertasConfig.tsx @@ -0,0 +1,178 @@ +import { useState, useEffect } from 'react' +import { Bell, Loader2, Plus, X, Zap, CheckCircle, AlertTriangle } from 'lucide-react' +import api from '../services/api' + +interface AlertConfig { + id: number + tipo: string + limite_percentual: number + centro_custo: string + ativo: boolean + criado_em?: string +} + +export default function AlertasConfig() { + const [loading, setLoading] = useState(true) + const [configs, setConfigs] = useState([]) + const [showForm, setShowForm] = useState(false) + const [saving, setSaving] = useState(false) + const [verifying, setVerifying] = useState(false) + const [verifyResult, setVerifyResult] = useState(null) + const [form, setForm] = useState({ tipo: 'orcamento_excedido', limite_percentual: 80, centro_custo: '' }) + + useEffect(() => { fetchData() }, []) + + const fetchData = async () => { + setLoading(true) + try { + const { data } = await api.get('/alertas/configs') + setConfigs(Array.isArray(data) ? data : data?.configs || []) + } catch (err) { + console.error('Error fetching alert configs:', err) + } finally { + setLoading(false) + } + } + + const handleCreate = async () => { + setSaving(true) + try { + await api.post('/alertas/configurar', form) + setShowForm(false) + setForm({ tipo: 'orcamento_excedido', limite_percentual: 80, centro_custo: '' }) + fetchData() + } catch (err) { + console.error('Error creating alert config:', err) + } finally { + setSaving(false) + } + } + + const handleVerify = async () => { + setVerifying(true) + setVerifyResult(null) + try { + const { data } = await api.post('/alertas/verificar') + setVerifyResult(data) + } catch (err) { + console.error('Error verifying alerts:', err) + } finally { + setVerifying(false) + } + } + + const handleToggle = async (config: AlertConfig) => { + try { + await api.put(`/alertas/configs/${config.id}`, { ...config, ativo: !config.ativo }) + setConfigs(configs.map(c => c.id === config.id ? { ...c, ativo: !c.ativo } : c)) + } catch (err) { + console.error('Error toggling alert:', err) + } + } + + if (loading) { + return
+ } + + return ( +
+
+
+

+ Configuração de Alertas +

+

Configure alertas inteligentes para monitoramento proativo.

+
+
+ + +
+
+ + {/* Verify Result */} + {verifyResult && ( +
+
+ + Verificação concluída +
+

+ {verifyResult.alertas_disparados ?? verifyResult.total ?? 0} alertas disparados. + {verifyResult.mensagem && ` ${verifyResult.mensagem}`} +

+
+ )} + + {/* Create Form */} + {showForm && ( +
+
+

Novo Alerta

+ +
+
+ + setForm({...form, limite_percentual: Number(e.target.value)})} className="input-field" /> + setForm({...form, centro_custo: e.target.value})} className="input-field" /> +
+ +
+ )} + + {/* Configs Table */} + {configs.length > 0 ? ( +
+ + + + + + + + + + + {configs.map(config => ( + + + + + + + ))} + +
TipoLimite %Centro de CustoAtivo
+
+ + {config.tipo.replace(/_/g, ' ')} +
+
{config.limite_percentual}%{config.centro_custo || '—'} + +
+
+ ) : ( +
+ +

Nenhum alerta configurado

+

Configure alertas para receber notificações proativas.

+
+ )} +
+ ) +} diff --git a/frontend/src/pages/Auditoria.tsx b/frontend/src/pages/Auditoria.tsx new file mode 100644 index 0000000..2efc6d2 --- /dev/null +++ b/frontend/src/pages/Auditoria.tsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from 'react' +import { Shield, Loader2, Download, Search, FileText, AlertTriangle, CheckCircle } from 'lucide-react' +import api from '../services/api' + +interface AuditLog { + id: number + timestamp: string + usuario: string + entidade: string + acao: string + detalhes: string +} + +interface ComplianceReport { + total_logs: number + acoes_criticas: number + usuarios_ativos: number + entidades_auditadas: number +} + +export default function Auditoria() { + const [loading, setLoading] = useState(true) + const [logs, setLogs] = useState([]) + const [compliance, setCompliance] = useState(null) + const [entity, setEntity] = useState('') + const [action, setAction] = useState('') + const [dateFrom, setDateFrom] = useState('') + const [dateTo, setDateTo] = useState('') + + useEffect(() => { fetchData() }, [entity, action, dateFrom, dateTo]) + + const fetchData = async () => { + setLoading(true) + try { + const params: any = {} + if (entity) params.entity = entity + if (action) params.action = action + if (dateFrom) params.date_from = dateFrom + if (dateTo) params.date_to = dateTo + const [logsRes, compRes] = await Promise.all([ + api.get('/audit/logs', { params }), + api.get('/audit/compliance-report') + ]) + setLogs(logsRes.data?.logs || logsRes.data || []) + setCompliance(compRes.data) + } catch (err) { + console.error('Error fetching audit data:', err) + } finally { + setLoading(false) + } + } + + const handleExport = (format: string) => { + const token = localStorage.getItem('token') + window.open(`/api/audit/export?format=${format}&token=${token}`, '_blank') + } + + if (loading) { + return ( +
+ +
+ ) + } + + return ( +
+
+
+

+ Auditoria & Compliance +

+

Registro de ações e conformidade do sistema.

+
+
+ + +
+
+ + {/* Compliance Summary */} + {compliance && ( +
+ {[ + { title: 'Total de Logs', value: compliance.total_logs, icon: , color: 'from-blue-500 to-blue-600' }, + { title: 'Ações Críticas', value: compliance.acoes_criticas, icon: , color: 'from-red-500 to-red-600' }, + { title: 'Usuários Ativos', value: compliance.usuarios_ativos, icon: , color: 'from-green-500 to-green-600' }, + { title: 'Entidades Auditadas', value: compliance.entidades_auditadas, icon: , color: 'from-purple-500 to-purple-600' }, + ].map((c, i) => ( +
+
+ {c.icon} +
+

{c.title}

+

{c.value}

+
+ ))} +
+ )} + + {/* Filters */} +
+
+ setEntity(e.target.value)} className="input-field text-sm" /> + setAction(e.target.value)} className="input-field text-sm" /> + setDateFrom(e.target.value)} className="input-field text-sm" /> + setDateTo(e.target.value)} className="input-field text-sm" /> +
+
+ + {/* Logs Table */} +
+ {Array.isArray(logs) && logs.length > 0 ? ( + + + + + + + + + + + + {logs.map(log => ( + + + + + + + + ))} + +
TimestampUsuárioEntidadeAçãoDetalhes
{new Date(log.timestamp).toLocaleString('pt-BR')}{log.usuario}{log.entidade} + {log.acao} + {log.detalhes}
+ ) : ( +
+ +

Nenhum log encontrado

+

Ajuste os filtros para visualizar registros de auditoria.

+
+ )} +
+
+ ) +} diff --git a/frontend/src/pages/Configuracao.tsx b/frontend/src/pages/Configuracao.tsx new file mode 100644 index 0000000..a072a98 --- /dev/null +++ b/frontend/src/pages/Configuracao.tsx @@ -0,0 +1,432 @@ +import { useState, useEffect } from 'react' +import { Settings, Plus, Edit2, Trash2, Loader2, X, Save } from 'lucide-react' +import api from '../services/api' +import Modal from '../components/Modal' + +type Tab = 'categorias' | 'centros_custo' | 'subcategorias' | 'locais' + +const tabs: { key: Tab; label: string }[] = [ + { key: 'categorias', label: 'Categorias' }, + { key: 'centros_custo', label: 'Centros de Custo' }, + { key: 'subcategorias', label: 'Subcategorias' }, + { key: 'locais', label: 'Locais' }, +] + +export default function Configuracao() { + const [activeTab, setActiveTab] = useState('categorias') + const [loading, setLoading] = useState(true) + const [items, setItems] = useState([]) + const [showModal, setShowModal] = useState(false) + const [editId, setEditId] = useState(null) + const [form, setForm] = useState({}) + const [saving, setSaving] = useState(false) + // For subcategorias + const [categorias, setCategorias] = useState([]) + + const endpoints: Record = { + categorias: '/categorias', + centros_custo: '/centros-custo', + subcategorias: '/subcategorias', + locais: '/locais', + } + + useEffect(() => { fetchItems(); fetchCategorias() }, [activeTab]) + + const fetchItems = async () => { + setLoading(true) + try { + const { data } = await api.get(endpoints[activeTab]) + setItems(data) + } catch { setItems([]) } + finally { setLoading(false) } + } + + const fetchCategorias = async () => { + try { + const { data } = await api.get('/categorias') + setCategorias(data) + } catch {} + } + + const getEmptyForm = (): any => { + switch (activeTab) { + case 'categorias': return { nome: '', criticidade_padrao: 'media', sla_dias: 30, tipo_investimento: '', tipo_manutencao: '', impacto_ambiental_esperado: '', potencial_geracao_residuos: '' } + case 'centros_custo': return { codigo: '', nome: '' } + case 'subcategorias': return { nome: '', categoria_id: '' } + case 'locais': return { nome: '', endereco: '', tipo_operacao_local: '', classificacao_impacto_ambiental: '', praticas_sustentaveis: [] } + } + } + + const openNew = () => { setEditId(null); setForm(getEmptyForm()); setShowModal(true) } + + const openEdit = (item: any) => { + setEditId(item.id) + switch (activeTab) { + case 'categorias': + setForm({ nome: item.nome, criticidade_padrao: item.criticidade_padrao || 'media', sla_dias: item.sla_dias || 30, tipo_investimento: item.tipo_investimento || '', tipo_manutencao: item.tipo_manutencao || '', impacto_ambiental_esperado: item.impacto_ambiental_esperado || '', potencial_geracao_residuos: item.potencial_geracao_residuos || '' }) + break + case 'centros_custo': + setForm({ codigo: item.codigo || '', nome: item.nome }) + break + case 'subcategorias': + setForm({ nome: item.nome, categoria_id: item.categoria_id || '' }) + break + case 'locais': + setForm({ nome: item.nome, endereco: item.endereco || '', tipo_operacao_local: item.tipo_operacao_local || '', classificacao_impacto_ambiental: item.classificacao_impacto_ambiental || '', praticas_sustentaveis: item.praticas_sustentaveis || [] }) + break + } + setShowModal(true) + } + + const handleSubmit = async () => { + setSaving(true) + try { + if (editId) { + await api.patch(`${endpoints[activeTab]}/${editId}`, form) + } else { + await api.post(endpoints[activeTab], form) + } + setShowModal(false) + fetchItems() + } catch (err) { + alert('Erro ao salvar') + } finally { + setSaving(false) + } + } + + const handleDelete = async (id: string) => { + if (!confirm('Tem certeza que deseja excluir?')) return + try { + await api.delete(`${endpoints[activeTab]}/${id}`) + fetchItems() + } catch { alert('Erro ao excluir') } + } + + const catMap: Record = {} + categorias.forEach(c => { catMap[c.id] = c.nome }) + + const renderFormFields = () => { + switch (activeTab) { + case 'categorias': + return ( + <> +
+ + setForm({ ...form, nome: e.target.value })} className="input-field" required /> +
+
+ + +
+
+ + +
+
+ + setForm({ ...form, sla_dias: Number(e.target.value) })} className="input-field" /> +
+
+

🌿 Campos ESG

+
+
+ + +
+
+ + +
+
+ + +
+ + ) + case 'centros_custo': + return ( + <> +
+ + setForm({ ...form, codigo: e.target.value })} className="input-field" required /> +
+
+ + setForm({ ...form, nome: e.target.value })} className="input-field" required /> +
+ + ) + case 'subcategorias': + return ( + <> +
+ + setForm({ ...form, nome: e.target.value })} className="input-field" required /> +
+
+ + +
+ + ) + case 'locais': + return ( + <> +
+ + setForm({ ...form, nome: e.target.value })} className="input-field" required /> +
+
+ + setForm({ ...form, endereco: e.target.value })} className="input-field" /> +
+
+

🌿 Campos ESG

+
+
+ + +
+
+ + +
+
+ +
+ {['Coleta Seletiva', 'Reuso de Água', 'Energia Renovável', 'Compostagem', 'Redução de Plástico'].map(p => ( + + ))} +
+
+ + ) + } + } + + const renderColumns = () => { + switch (activeTab) { + case 'categorias': + return ( + <> + Nome + Tipo Invest. + Criticidade + SLA + Manutenção + Impacto Amb. + Ações + + ) + case 'centros_custo': + return ( + <> + Código + Nome + Ações + + ) + case 'subcategorias': + return ( + <> + Nome + Categoria + Ações + + ) + case 'locais': + return ( + <> + Nome + Endereço + Operação + Impacto Amb. + Ações + + ) + } + } + + const renderRow = (item: any) => { + switch (activeTab) { + case 'categorias': + return ( + <> + {item.nome} + + {item.tipo_investimento ? ( + + {item.tipo_investimento === 'capex' ? 'Capex' : 'Opex'} + + ) : '-'} + + {item.criticidade_padrao || '-'} + {item.sla_dias} dias + + {item.tipo_manutencao ? ( + + {item.tipo_manutencao} + + ) : '-'} + + + {item.impacto_ambiental_esperado ? ( + + {item.impacto_ambiental_esperado} + + ) : '-'} + + + ) + case 'centros_custo': + return ( + <> + {item.codigo} + {item.nome} + + ) + case 'subcategorias': + return ( + <> + {item.nome} + {catMap[item.categoria_id] || '-'} + + ) + case 'locais': + return ( + <> + {item.nome} + {item.endereco || '-'} + {item.tipo_operacao_local || '-'} + + {item.classificacao_impacto_ambiental ? ( + + {item.classificacao_impacto_ambiental} + + ) : '-'} + + + ) + } + } + + return ( +
+
+
+

+ Configuração +

+

Gerencie as tabelas de apoio do sistema

+
+ +
+ + {/* Tabs */} +
+ {tabs.map(tab => ( + + ))} +
+ + {/* Table */} + {loading ? ( +
+ ) : ( +
+
+ + + {renderColumns()} + + + {items.map(item => ( + + {renderRow(item)} + + + ))} + +
+
+ + +
+
+
+ {items.length === 0 && ( +

Nenhum registro encontrado

+ )} +
+ )} + + {/* Create/Edit Modal */} + setShowModal(false)} title={editId ? 'Editar' : 'Novo Registro'} onSubmit={handleSubmit} submitLabel={editId ? 'Salvar' : 'Criar'} loading={saving}> + {renderFormFields()} + +
+ ) +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 58bf14c..6b791e6 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import { Wallet, TrendingUp, @@ -9,60 +9,76 @@ import { CheckCircle2, AlertCircle, ArrowUpRight, - Loader2 + Loader2, + Leaf, + Shield, + Search, + X, + Filter } from 'lucide-react' -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts' +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend, LineChart, Line } from 'recharts' import api from '../services/api' -import { User } from '../types' +import { User, DashboardIndicadores } from '../types' +import Modal from '../components/Modal' interface StatsCard { title: string; value: string | number; - change?: string; - changeType?: 'positive' | 'negative' | 'neutral'; icon: React.ReactNode; color: string; + statusKey: string; } -const monthlyData = [ - { name: 'Jan', previsto: 4000, realizado: 3800 }, - { name: 'Fev', previsto: 3500, realizado: 3200 }, - { name: 'Mar', previsto: 4200, realizado: 4100 }, - { name: 'Abr', previsto: 3800, realizado: 3900 }, - { name: 'Mai', previsto: 4500, realizado: 4200 }, - { name: 'Jun', previsto: 4000, realizado: 3700 }, -] - -const categoryData = [ - { name: 'Manutenção', value: 35, color: '#E65100' }, - { name: 'Limpeza', value: 25, color: '#1A237E' }, - { name: 'Segurança', value: 20, color: '#FF8F00' }, - { name: 'Outros', value: 20, color: '#757575' }, -] - -const recentActivities = [ - { id: 1, action: 'Nova demanda criada', description: 'Manutenção ar condicionado - Bloco A', time: 'Há 2 horas', status: 'pending' }, - { id: 2, action: 'Ordem de serviço aprovada', description: 'OS-2024-0156 - Troca de lâmpadas', time: 'Há 4 horas', status: 'success' }, - { id: 3, action: 'Fornecedor cadastrado', description: 'Tech Solutions Ltda', time: 'Há 6 horas', status: 'info' }, - { id: 4, action: 'Orçamento atualizado', description: 'Categoria: Manutenção Predial', time: 'Há 8 horas', status: 'warning' }, -] +type DashTab = 'geral' | 'esg' export default function Dashboard() { const [loading, setLoading] = useState(true) - const [stats, setStats] = useState(null) + const [stats, setStats] = useState(null) const [user, setUser] = useState(null) + const [activeTab, setActiveTab] = useState('geral') + const [esgData, setEsgData] = useState(null) + const [esgLoading, setEsgLoading] = useState(false) + + // Chart filter state + const [chartFilter, setChartFilter] = useState<'todos' | 'centro_custo' | 'categoria'>('todos') + const [chartFilterId, setChartFilterId] = useState('') + const [chartData, setChartData] = useState([]) + const [centrosCusto, setCentrosCusto] = useState<{id:string;nome:string}[]>([]) + const [categorias, setCategorias] = useState<{id:string;nome:string}[]>([]) + + // Category chart data (from backend) + const [categoryData, setCategoryData] = useState([]) + + // Card drill-down modal + const [drillModal, setDrillModal] = useState(false) + const [drillTitle, setDrillTitle] = useState('') + const [drillData, setDrillData] = useState([]) + const [drillLoading, setDrillLoading] = useState(false) + + // Search + const [searchTerm, setSearchTerm] = useState('') + const [searchResults, setSearchResults] = useState(null) + const [searchLoading, setSearchLoading] = useState(false) useEffect(() => { const userData = localStorage.getItem('user') - if (userData) { - setUser(JSON.parse(userData)) - } + if (userData) setUser(JSON.parse(userData)) fetchDashboard() + fetchLookups() + fetchCategoryData() }, []) + useEffect(() => { + if (activeTab === 'esg' && !esgData) fetchEsg() + }, [activeTab]) + + useEffect(() => { + fetchChartData() + }, [chartFilter, chartFilterId]) + const fetchDashboard = async () => { try { - const { data } = await api.get('/dashboard') + const { data } = await api.get('/dashboard/indicadores') setStats(data) } catch (err) { console.error('Error fetching dashboard:', err) @@ -71,39 +87,93 @@ export default function Dashboard() { } } + const fetchLookups = async () => { + try { + const [catRes, ccRes] = await Promise.all([ + api.get('/categorias').catch(() => ({ data: [] })), + api.get('/centros-custo').catch(() => ({ data: [] })), + ]) + setCategorias((catRes.data || []).map((c: any) => ({ id: c.id, nome: c.nome }))) + setCentrosCusto((ccRes.data || []).map((c: any) => ({ id: c.id, nome: c.nome }))) + } catch {} + } + + const fetchChartData = async () => { + try { + const params: any = { ano: 2026 } + if (chartFilter === 'centro_custo' && chartFilterId) params.centro_custo_id = chartFilterId + if (chartFilter === 'categoria' && chartFilterId) params.categoria_id = chartFilterId + const { data } = await api.get('/dashboard/consumo-orcamento', { params }) + setChartData(data) + } catch (err) { + console.error('Error fetching chart data:', err) + } + } + + const fetchCategoryData = async () => { + try { + const { data } = await api.get('/dashboard/categorias-quantidade') + setCategoryData(data) + } catch (err) { + console.error('Error fetching category data:', err) + } + } + + const fetchEsg = async () => { + setEsgLoading(true) + try { + const { data } = await api.get('/dashboard/esg') + setEsgData(data) + } catch (err) { + console.error('Error fetching ESG:', err) + } finally { + setEsgLoading(false) + } + } + + const handleCardClick = async (statusKey: string, title: string) => { + setDrillTitle(title) + setDrillModal(true) + setDrillLoading(true) + try { + const { data } = await api.get('/dashboard/demandas-detalhe', { params: { status: statusKey } }) + setDrillData(data) + } catch (err) { + console.error('Error fetching drill-down:', err) + setDrillData([]) + } finally { + setDrillLoading(false) + } + } + + const handleSearch = useCallback(async (term: string) => { + if (term.trim().length < 2) { + setSearchResults(null) + return + } + setSearchLoading(true) + try { + const { data } = await api.get('/dashboard/busca', { params: { q: term } }) + setSearchResults(data) + } catch { + setSearchResults(null) + } finally { + setSearchLoading(false) + } + }, []) + + useEffect(() => { + const timer = setTimeout(() => handleSearch(searchTerm), 400) + return () => clearTimeout(timer) + }, [searchTerm, handleSearch]) + + const formatCurrency = (v: number) => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v || 0) + const statsCards: StatsCard[] = [ - { - title: 'Orçamento Total', - value: stats?.total_orcamento ? `${(stats.total_orcamento / 1000).toFixed(0)}K` : '0', - change: '+12%', - changeType: 'positive', - icon: , - color: 'from-primary to-accent' - }, - { - title: 'Total Gasto', - value: stats?.total_gasto ? `${(stats.total_gasto / 1000).toFixed(0)}K` : '0', - change: '-5%', - changeType: 'positive', - icon: , - color: 'from-secondary to-secondary-light' - }, - { - title: 'Economia', - value: stats?.economia ? `${(stats.economia / 1000).toFixed(0)}K` : '0', - change: '+8%', - changeType: 'positive', - icon: , - color: 'from-green-500 to-emerald-500' - }, - { - title: 'Pendências', - value: stats?.pendencias || stats?.demandas_pendentes || '0', - change: '-3', - changeType: 'neutral', - icon: , - color: 'from-amber-500 to-orange-500' - }, + { title: 'Demandas Abertas', value: stats?.demandas_abertas ?? 0, icon: , color: 'from-primary to-accent', statusKey: 'abertas' }, + { title: 'Em Cotação', value: stats?.em_cotacao ?? 0, icon: , color: 'from-secondary to-secondary-light', statusKey: 'em_cotacao' }, + { title: 'Em Aprovação', value: stats?.em_aprovacao ?? 0, icon: , color: 'from-amber-500 to-orange-500', statusKey: 'em_aprovacao' }, + { title: 'OS Ativas', value: stats?.os_ativas ?? 0, icon: , color: 'from-green-500 to-emerald-500', statusKey: 'concluidas' }, ] if (loading) { @@ -114,9 +184,17 @@ export default function Dashboard() { ) } + const manutencaoPieData = esgData ? [ + { name: 'Preventiva', value: esgData.total_preventivas, color: '#1A7A4C' }, + { name: 'Corretiva', value: esgData.total_corretivas, color: '#FF8F00' }, + { name: 'Emergencial', value: esgData.total_emergenciais, color: '#E53935' }, + ] : [] + + const filterOptions = chartFilter === 'centro_custo' ? centrosCusto : chartFilter === 'categoria' ? categorias : [] + return (
- {/* Welcome header */} + {/* Welcome header + Search */}

@@ -124,174 +202,359 @@ export default function Dashboard() {

Aqui está o resumo das suas operações de facilities.

-
- Última atualização: - Agora -
-
- - {/* Stats Cards */} -
- {statsCards.map((card, index) => ( -
-
-
- {card.icon} -
- {card.change && ( - - {card.change} - +
+ + setSearchTerm(e.target.value)} + className="input-field pl-10 pr-8 !py-2 text-sm" + /> + {searchTerm && ( + + )} + {/* Search Results Dropdown */} + {searchResults && ( +
+ {searchResults.demandas?.length > 0 ? ( +
+

Demandas

+ {searchResults.demandas.map((d: any) => ( +
+ +
+

{d.numero ? `#${d.numero} - ` : ''}{d.titulo}

+

{d.categoria} · {d.status}

+
+
+ ))} +
+ ) : ( +
Nenhum resultado encontrado
)}
-

{card.title}

-

{card.value}

-
- ))} -
- - {/* Charts Row */} -
- {/* Bar Chart */} -
-
-
-

Orçamento vs Realizado

-

Comparativo mensal

-
- -
-
- - - - - - - - - - -
-
- - {/* Pie Chart */} -
-
-

Por Categoria

-

Distribuição de gastos

-
-
- - - - {categoryData.map((entry, index) => ( - - ))} - - {value}} - /> - - -
+ )}
- {/* Quick Stats & Activity */} -
- {/* Quick Stats */} -
-

Resumo Rápido

-
-
-
-
- -
- Demandas Abertas -
- {stats?.demandas_pendentes || 12} -
-
-
-
- -
- OS Concluídas -
- {stats?.ordens_concluidas || 48} -
-
-
-
- -
- Fornecedores Ativos -
- {stats?.fornecedores_ativos || 15} -
-
-
+ {/* Tabs */} +
+ + +
- {/* Recent Activity */} -
-
-

Atividade Recente

- -
-
- {recentActivities.map((activity) => ( -
-
- {activity.status === 'success' ? : - activity.status === 'warning' ? : - activity.status === 'info' ? : - } + {activeTab === 'geral' ? ( + <> + {/* Stats Cards - clickable */} +
+ {statsCards.map((card, index) => ( +
handleCardClick(card.statusKey, card.title)} + > +
+
+ {card.icon} +
+
-
-

{activity.action}

-

{activity.description}

-
- {activity.time} +

{card.title}

+

{card.value}

))}
+ + {/* Charts Row */} +
+
+
+
+

Orçamento vs Realizado

+

Comparativo mensal

+
+ {/* Filter Controls */} +
+ + + {chartFilter !== 'todos' && ( + + )} +
+
+
+ + + + + + formatCurrency(value)} /> + + + + +
+
+
+
+

Por Categoria

+

Quantidade de demandas

+
+
+ {categoryData.length > 0 ? ( + + + `${value}`}> + {categoryData.map((entry: any, index: number) => ())} + + {value}} /> + [`${value} demandas`, name]} /> + + + ) : ( +
Sem dados
+ )} +
+
+
+ + {/* Quick Stats & Activity */} +
+
+

Resumo Rápido

+
+
+
+
+ Demandas Abertas +
+ {stats?.demandas_abertas ?? 0} +
+
+
+
+ Pendentes +
+ {stats?.pendentes ?? 0} +
+
+
+
+ Alertas +
+ {stats?.alertas ?? 0} +
+
+
+
+
+

Atividade Recente

+
+
+ +

Atividades recentes aparecerão aqui.

+
+
+
+ + ) : ( + /* ESG Tab */ + esgLoading ? ( +
+ ) : esgData ? ( + <> +
+
+
+
+ +
+
+

Preventivas

+

{esgData.pct_preventivas}%

+

{esgData.total_preventivas} demandas

+
+
+
+
+
+
+ +
+
+

Corretivas

+

{esgData.pct_corretivas}%

+

{esgData.total_corretivas} demandas

+
+
+
+
+
+
+ +
+
+

Emergenciais

+

{esgData.pct_emergenciais}%

+

{esgData.total_emergenciais} demandas

+
+
+
+
+
+
+ +
+
+

Fornecedores ESG+

+

{esgData.pct_fornecedores_esg_bom}%

+

{esgData.fornecedores_esg_intermediario_avancado}/{esgData.total_fornecedores}

+
+
+
+
+
+
+

Tipo de Manutenção

+
+ + + + {manutencaoPieData.map((entry: any, index: number) => ())} + + {value}} /> + + + +
+
+
+

Evolução Manutenção Preventiva

+
+ + + + + + + + + +
+
+
+
+
+ +

Demandas com Alto Impacto Ambiental

+ {esgData.demandas_alto_impacto_count} +
+ {esgData.demandas_alto_impacto.length === 0 ? ( +

Nenhuma demanda com alto impacto ambiental.

+ ) : ( +
+ {esgData.demandas_alto_impacto.map((d: any) => ( +
+ +
+

{d.numero ? `#${d.numero} - ` : ''}{d.titulo}

+
+ {d.status} +
+ ))} +
+ )} +
+ {esgData.fornecedores_esg_basico > 0 && ( +
+
+ +
+

Atenção: Fornecedores ESG Básico

+

{esgData.fornecedores_esg_basico} fornecedor(es) com classificação ESG Básico.

+
+
+
+ )} + + ) : ( +
+ +

Dados ESG não disponíveis.

+
+ ) + )} + + {/* Drill-down Modal */} + {drillModal && ( +
+
+
+

{drillTitle}

+ +
+
+ {drillLoading ? ( +
+ ) : drillData.length === 0 ? ( +

Nenhuma demanda encontrada.

+ ) : ( +
+ {drillData.map((d: any) => ( +
+
+ +
+
+

{d.numero ? `#${d.numero} - ` : ''}{d.titulo}

+
+ {d.categoria} + · + {d.data ? new Date(d.data).toLocaleDateString('pt-BR') : '-'} +
+
+
+ {d.status} + {d.valor_estimado != null && ( +

{formatCurrency(d.valor_estimado)}

+ )} +
+
+ ))} +
+ )} +
+
-
+ )}
) } diff --git a/frontend/src/pages/Demandas.tsx b/frontend/src/pages/Demandas.tsx index 3661b34..cc7e3d9 100644 --- a/frontend/src/pages/Demandas.tsx +++ b/frontend/src/pages/Demandas.tsx @@ -1,44 +1,54 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { - FileText, - Search, - Plus, - Filter, - Eye, - Edit2, - Trash2, - X, - Loader2, - AlertCircle, - Clock, - CheckCircle2, - ChevronDown + FileText, Search, Plus, Eye, Edit2, Trash2, X, Loader2, + AlertCircle, Clock, CheckCircle2, ChevronDown, PlayCircle, + Upload, Download, Paperclip, File, ClipboardList, DollarSign } from 'lucide-react' import api from '../services/api' -import { Demanda } from '../types' +import { Demanda, Subcategoria, DocumentoFile, OrdemServico } from '../types' +import { useLookups } from '../hooks/useLookups' +import Modal from '../components/Modal' const statusConfig: Record = { + 'aberta': { label: 'Aberta', class: 'badge-warning', icon: }, + 'rascunho': { label: 'Rascunho', class: 'badge-neutral', icon: }, + 'em_escopo': { label: 'Em Escopo', class: 'badge-info', icon: }, + 'em_cotacao': { label: 'Em Cotação', class: 'badge-info', icon: }, + 'propostas_recebidas': { label: 'Propostas Recebidas', class: 'badge-info', icon: }, + 'em_aprovacao': { label: 'Em Aprovação', class: 'badge-warning', icon: }, + 'aprovada': { label: 'Aprovada', class: 'badge-success', icon: }, + 'em_execucao': { label: 'Em Execução', class: 'badge-info', icon: }, + 'concluida': { label: 'Concluída', class: 'badge-neutral', icon: }, + 'cancelada': { label: 'Cancelada', class: 'badge-error', icon: }, 'pendente': { label: 'Pendente', class: 'badge-warning', icon: }, 'em_analise': { label: 'Em Análise', class: 'badge-info', icon: }, - 'aprovada': { label: 'Aprovada', class: 'badge-success', icon: }, 'rejeitada': { label: 'Rejeitada', class: 'badge-error', icon: }, - 'concluida': { label: 'Concluída', class: 'badge-neutral', icon: }, } -const prioridadeConfig: Record = { +const criticidadeConfig: Record = { 'baixa': { label: 'Baixa', class: 'text-gray bg-gray-100' }, 'media': { label: 'Média', class: 'text-amber-700 bg-amber-100' }, 'alta': { label: 'Alta', class: 'text-red-700 bg-red-100' }, 'urgente': { label: 'Urgente', class: 'text-red-700 bg-red-200 animate-pulse' }, + 'critica': { label: 'Crítica', class: 'text-red-700 bg-red-200 animate-pulse' }, } -const mockDemandas: Demanda[] = [ - { id: 1, titulo: 'Manutenção Ar Condicionado', descricao: 'Ar condicionado do bloco A não está funcionando', status: 'pendente', prioridade: 'alta', solicitante_id: 1, solicitante_nome: 'Maria Silva', data_criacao: '2024-01-15' }, - { id: 2, titulo: 'Troca de Lâmpadas', descricao: 'Lâmpadas queimadas no corredor do 3º andar', status: 'em_analise', prioridade: 'media', solicitante_id: 2, solicitante_nome: 'João Santos', data_criacao: '2024-01-14' }, - { id: 3, titulo: 'Vazamento Banheiro', descricao: 'Vazamento na torneira do banheiro masculino', status: 'aprovada', prioridade: 'urgente', solicitante_id: 3, solicitante_nome: 'Ana Oliveira', data_criacao: '2024-01-13' }, - { id: 4, titulo: 'Pintura Sala Reunião', descricao: 'Paredes da sala de reunião precisam de pintura', status: 'concluida', prioridade: 'baixa', solicitante_id: 1, solicitante_nome: 'Maria Silva', data_criacao: '2024-01-10' }, - { id: 5, titulo: 'Reparo Elevador', descricao: 'Elevador social com barulho estranho', status: 'pendente', prioridade: 'alta', solicitante_id: 4, solicitante_nome: 'Carlos Lima', data_criacao: '2024-01-16' }, -] +const tipoDocIcon: Record = { + 'planta': '📋', + 'foto': '📸', + 'laudo': '📄', + 'outro': '📎', +} + +const osStatusConfig: Record = { + 'emitida': { label: 'Emitida', class: 'badge-warning' }, + 'em_cotacao': { label: 'Em Cotação', class: 'badge-info' }, + 'em_execucao': { label: 'Em Execução', class: 'badge-info' }, + 'concluida': { label: 'Concluída', class: 'badge-success' }, + 'cancelada': { label: 'Cancelada', class: 'badge-error' }, +} + +const emptyForm = { titulo: '', descricao: '', criticidade: 'media', categoria_id: '', subcategoria_id: '', local_id: '', centro_custo_id: '', data_desejada: '', valor_estimado: '', impacto_ambiental_demanda: '', justificativa_manutencao_emergencial: '' } export default function Demandas() { const [loading, setLoading] = useState(true) @@ -46,265 +56,519 @@ export default function Demandas() { const [searchTerm, setSearchTerm] = useState('') const [filterStatus, setFilterStatus] = useState('todos') const [showModal, setShowModal] = useState(false) - const [selectedDemanda, setSelectedDemanda] = useState(null) - const [formData, setFormData] = useState({ titulo: '', descricao: '', prioridade: 'media' }) + const [showDetail, setShowDetail] = useState(null) + const [editId, setEditId] = useState(null) + const [form, setForm] = useState(emptyForm) + const [saving, setSaving] = useState(false) + const [subcategorias, setSubcategorias] = useState([]) + const [subcategoriaMap, setSubcategoriaMap] = useState>({}) + const [allSubcategorias, setAllSubcategorias] = useState([]) + // Documents + const [detailDocs, setDetailDocs] = useState([]) + const [docsLoading, setDocsLoading] = useState(false) + const [uploading, setUploading] = useState(false) + const [uploadTipo, setUploadTipo] = useState('outro') + const [dragOver, setDragOver] = useState(false) + const fileInputRef = useRef(null) + const [docCounts, setDocCounts] = useState>({}) + // OS linked + const [detailOS, setDetailOS] = useState([]) + const [osLoading, setOsLoading] = useState(false) + const [showOS, setShowOS] = useState(false) - useEffect(() => { - fetchDemandas() - }, []) + const { categoriaMap, centrosCustoMap, locaisMap, categorias, centrosCusto, locais, fornecedoresMap, loading: lookupsLoading } = useLookups() + + useEffect(() => { fetchDemandas(); fetchAllSubcategorias() }, []) const fetchDemandas = async () => { try { const { data } = await api.get('/demandas') - setDemandas(data.length > 0 ? data : mockDemandas) + setDemandas(data) } catch (err) { console.error('Error fetching demandas:', err) - setDemandas(mockDemandas) } finally { setLoading(false) } } + const fetchAllSubcategorias = async () => { + try { + const { data } = await api.get('/subcategorias') + setAllSubcategorias(data) + const map: Record = {} + data.forEach((s: Subcategoria) => { map[s.id] = s.nome }) + setSubcategoriaMap(map) + } catch (err) { + console.error('Error fetching subcategorias:', err) + } + } + + const fetchSubcategorias = async (categoriaId: string) => { + if (!categoriaId) { setSubcategorias([]); return } + try { + const { data } = await api.get(`/subcategorias?categoria_id=${categoriaId}`) + setSubcategorias(data) + } catch (err) { + setSubcategorias([]) + } + } + + const fetchDetailDocs = async (demandaId: string) => { + setDocsLoading(true) + try { + const { data } = await api.get(`/demandas/${demandaId}/documentos`) + setDetailDocs(data) + } catch { setDetailDocs([]) } + finally { setDocsLoading(false) } + } + + const fetchDetailOS = async (demandaId: string) => { + setOsLoading(true) + try { + const { data } = await api.get(`/ordens-servico/by-demanda/${demandaId}`) + setDetailOS(data) + } catch { setDetailOS([]) } + finally { setOsLoading(false) } + } + + useEffect(() => { + if (demandas.length === 0) return + const fetchCounts = async () => { + const counts: Record = {} + await Promise.all(demandas.map(async (d) => { + try { + const { data } = await api.get(`/demandas/${d.id}/documentos`) + counts[d.id] = data.length + } catch { counts[d.id] = 0 } + })) + setDocCounts(counts) + } + fetchCounts() + }, [demandas]) + const filteredDemandas = demandas.filter(demanda => { - const matchesSearch = demanda.titulo.toLowerCase().includes(searchTerm.toLowerCase()) || - demanda.descricao.toLowerCase().includes(searchTerm.toLowerCase()) + const matchesSearch = (demanda.titulo || '').toLowerCase().includes(searchTerm.toLowerCase()) || + (demanda.descricao || '').toLowerCase().includes(searchTerm.toLowerCase()) const matchesStatus = filterStatus === 'todos' || demanda.status === filterStatus return matchesSearch && matchesStatus }) - const handleOpenModal = (demanda?: Demanda) => { - if (demanda) { - setSelectedDemanda(demanda) - setFormData({ titulo: demanda.titulo, descricao: demanda.descricao, prioridade: demanda.prioridade }) - } else { - setSelectedDemanda(null) - setFormData({ titulo: '', descricao: '', prioridade: 'media' }) - } + const openNew = () => { setEditId(null); setForm(emptyForm); setSubcategorias([]); setShowModal(true) } + const openEdit = (d: Demanda) => { + setEditId(d.id) + setForm({ + titulo: d.titulo, descricao: d.descricao, criticidade: d.criticidade || d.prioridade || 'media', + categoria_id: d.categoria_id || '', subcategoria_id: d.subcategoria_id || '', + local_id: d.local_id || '', centro_custo_id: d.centro_custo_id || '', + data_desejada: d.data_desejada || '', + valor_estimado: d.valor_estimado ? String(d.valor_estimado) : '', + impacto_ambiental_demanda: (d as any).impacto_ambiental_demanda || '', + justificativa_manutencao_emergencial: (d as any).justificativa_manutencao_emergencial || '', + }) + if (d.categoria_id) fetchSubcategorias(d.categoria_id) setShowModal(true) } - const handleCloseModal = () => { - setShowModal(false) - setSelectedDemanda(null) - setFormData({ titulo: '', descricao: '', prioridade: 'media' }) + const handleCategoriaChange = (categoriaId: string) => { + setForm({ ...form, categoria_id: categoriaId, subcategoria_id: '' }) + fetchSubcategorias(categoriaId) } - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - // Mock save - if (selectedDemanda) { - setDemandas(demandas.map(d => d.id === selectedDemanda.id ? { ...d, ...formData } : d)) - } else { - const newDemanda: Demanda = { - id: Date.now(), - ...formData, - status: 'pendente', - solicitante_id: 1, - solicitante_nome: 'Usuário Atual', - data_criacao: new Date().toISOString().split('T')[0] + const handleSubmit = async () => { + setSaving(true) + try { + const payload: any = { ...form } + payload.valor_estimado = payload.valor_estimado ? Number(payload.valor_estimado) : null + if (editId) { + await api.patch(`/demandas/${editId}`, payload) + } else { + await api.post('/demandas', { ...payload, status: 'rascunho' }) } - setDemandas([newDemanda, ...demandas]) + setShowModal(false) + fetchDemandas() + } catch (err) { + console.error('Error saving:', err) + alert('Erro ao salvar demanda') + } finally { + setSaving(false) } - handleCloseModal() } - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString('pt-BR') + const handleDelete = async (id: string) => { + if (!confirm('Tem certeza que deseja excluir esta demanda?')) return + try { + await api.delete(`/demandas/${id}`) + fetchDemandas() + } catch (err) { alert('Erro ao excluir') } } - if (loading) { - return ( -
- -
- ) + const handleChangeOSStatus = async (osId: string, status: string) => { + try { + await api.post(`/ordens-servico/${osId}/status`, { status }) + if (showDetail) fetchDetailOS(showDetail.id) + } catch { alert('Erro ao alterar status da OS') } + } + + const handleFileUpload = async (files: FileList | null) => { + if (!files || !showDetail) return + setUploading(true) + try { + for (const file of Array.from(files)) { + const formData = new FormData() + formData.append('file', file) + formData.append('tipo', uploadTipo) + await api.post(`/demandas/${showDetail.id}/documentos`, formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) + } + fetchDetailDocs(showDetail.id) + setDocCounts(prev => ({ ...prev, [showDetail.id]: (prev[showDetail.id] || 0) + files.length })) + } catch (err) { + alert('Erro ao fazer upload') + } finally { + setUploading(false) + } + } + + const handleDocDownload = (doc: DocumentoFile) => { + window.open(`/api/documentos/${doc.id}/download`, '_blank') + } + + const handleDocDelete = async (doc: DocumentoFile) => { + if (!confirm(`Excluir ${doc.nome_arquivo}?`)) return + try { + await api.delete(`/documentos/${doc.id}`) + if (showDetail) { + fetchDetailDocs(showDetail.id) + setDocCounts(prev => ({ ...prev, [showDetail.id]: Math.max(0, (prev[showDetail.id] || 1) - 1) })) + } + } catch { alert('Erro ao excluir documento') } + } + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + setDragOver(false) + handleFileUpload(e.dataTransfer.files) + }, [showDetail, uploadTipo]) + + const formatDate = (dateStr?: string | null) => { + if (!dateStr) return '-' + try { return new Date(dateStr).toLocaleDateString('pt-BR') } catch { return '-' } + } + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + } + + const formatCurrency = (v: number | null | undefined) => { + if (v == null) return '-' + return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v) + } + + const getCriticidade = (d: Demanda) => d.criticidade || d.prioridade || '' + const topStatuses = ['aberta', 'em_cotacao', 'em_aprovacao', 'em_execucao'] + + const openDetail = (d: Demanda) => { + setShowDetail(d) + setShowOS(false) + fetchDetailDocs(d.id) + fetchDetailOS(d.id) + } + + if (loading || lookupsLoading) { + return
} return (
- {/* Header */}

Demandas

Gerencie as solicitações de facilities

-
- {/* Stats */}
- {Object.entries(statusConfig).slice(0, 4).map(([key, config]) => { + {topStatuses.map((key) => { + const config = statusConfig[key] + if (!config) return null const count = demandas.filter(d => d.status === key).length return ( - ) })}
- {/* Filters */}
- setSearchTerm(e.target.value)} - className="input-field pl-12" - /> + setSearchTerm(e.target.value)} className="input-field pl-12" />
- setFilterStatus(e.target.value)} className="input-field appearance-none pr-10 w-full sm:w-48"> - {Object.entries(statusConfig).map(([key, config]) => ( - - ))} + {Object.entries(statusConfig).map(([key, config]) => ())}
- {/* Demandas List */}
- {filteredDemandas.map((demanda) => ( -
-
-
-
-
- -
-
-
-

{demanda.titulo}

- - {statusConfig[demanda.status]?.icon} - {statusConfig[demanda.status]?.label || demanda.status} - - - {prioridadeConfig[demanda.prioridade]?.label || demanda.prioridade} - + {filteredDemandas.map((demanda) => { + const crit = getCriticidade(demanda) + const docsCount = docCounts[demanda.id] || 0 + return ( +
+
+
+
+
+
-

{demanda.descricao}

-
- Solicitante: {demanda.solicitante_nome} - - {formatDate(demanda.data_criacao)} +
+
+

{demanda.numero ? `#${demanda.numero} - ` : ''}{demanda.titulo}

+ + {statusConfig[demanda.status]?.icon}{statusConfig[demanda.status]?.label || demanda.status} + + {crit && {criticidadeConfig[crit]?.label || crit}} +
+

{demanda.descricao}

+
+ {demanda.categoria_id && Cat: {categoriaMap[demanda.categoria_id] || '-'}} + {demanda.subcategoria_id && Sub: {subcategoriaMap[demanda.subcategoria_id] || '-'}} + {demanda.local_id && Local: {locaisMap[demanda.local_id] || '-'}} + {demanda.centro_custo_id && CC: {centrosCustoMap[demanda.centro_custo_id] || '-'}} + {demanda.valor_estimado && {formatCurrency(demanda.valor_estimado)}} + {demanda.data_desejada && 🗓️ {formatDate(demanda.data_desejada)}} + {formatDate(demanda.created_at || demanda.data_criacao)} + {docsCount > 0 && {docsCount}} +
-
-
- - - +
+ + + +
-
- ))} - + ) + })} {filteredDemandas.length === 0 && ( -
- -

Nenhuma demanda encontrada

-
+

Nenhuma demanda encontrada

)}
- {/* Modal */} - {showModal && ( -
-
-
-

- {selectedDemanda ? 'Editar Demanda' : 'Nova Demanda'} -

- -
-
-
- - setFormData({ ...formData, titulo: e.target.value })} - className="input-field" - placeholder="Título da demanda" - required - /> -
-
- -