[MVPTN-37] feat(CRUD): Implementação inicial do CRUD de serviços de balcão

This commit is contained in:
Keven Willian Pereira de Souza 2025-10-27 15:29:51 -03:00
parent 8e035546e9
commit e39422776e
56 changed files with 3606 additions and 99 deletions

482
package-lock.json generated
View file

@ -18,6 +18,8 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
@ -30,7 +32,9 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"cookies-next": "^6.1.0",
"date-fns": "^4.1.0",
"faker-js": "^1.0.0",
"framer-motion": "^12.23.24",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
@ -42,6 +46,7 @@
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"react-masked-text": "^1.0.5",
"recharts": "^3.3.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tinymce": "^8.1.2",
@ -49,6 +54,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/date-fns": "^2.5.3",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
@ -1962,6 +1968,30 @@
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
"integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
@ -1993,6 +2023,37 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
"integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
@ -2353,6 +2414,32 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@reduxjs/toolkit": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz",
"integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -2360,6 +2447,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
@ -2714,6 +2807,76 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/date-fns": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz",
"integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -2790,6 +2953,12 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
@ -3900,6 +4069,127 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -3961,6 +4251,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@ -3979,6 +4279,12 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -4280,6 +4586,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-toolkit": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz",
"integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@ -4927,6 +5243,12 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/faker-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/faker-js/-/faker-js-1.0.0.tgz",
@ -5081,6 +5403,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/framer-motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -5412,6 +5761,16 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -5464,6 +5823,15 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -6530,6 +6898,21 @@
"node": ">= 18"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -7179,6 +7562,29 @@
"integrity": "sha512-WichrlCXehL0apIfIgOdi2mjBE03tdMi8wXF+DhHe2ySWYxXCkP88aqDBaJZWUMa3Jp8p2h71u7TpC7EzEjXYw==",
"license": "ISC"
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-remove-scroll": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
@ -7248,6 +7654,48 @@
}
}
},
"node_modules/recharts": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz",
"integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==",
"license": "MIT",
"dependencies": {
"@reduxjs/toolkit": "1.x.x || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@ -7292,6 +7740,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -7944,6 +8398,12 @@
"node": ">=18"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@ -8336,6 +8796,28 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -20,6 +20,8 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
@ -32,7 +34,9 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"cookies-next": "^6.1.0",
"date-fns": "^4.1.0",
"faker-js": "^1.0.0",
"framer-motion": "^12.23.24",
"input-otp": "^1.4.2",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
@ -44,6 +48,7 @@
"react-dom": "19.1.0",
"react-hook-form": "^7.62.0",
"react-masked-text": "^1.0.5",
"recharts": "^3.3.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tinymce": "^8.1.2",
@ -51,6 +56,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/date-fns": "^2.5.3",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",

View file

@ -0,0 +1,5 @@
export default function TCensecPage() {
return (
<div></div>
);
}

View file

@ -0,0 +1,9 @@
'use client';
import TImovelDashboard from "@/packages/administrativo/components/TImovel/TImovelDashboard";
export default function TImovelDashboardPage() {
return (
<TImovelDashboard />
);
}

View file

@ -0,0 +1,9 @@
'use client';
import TPessoaDashboard from "@/packages/administrativo/components/TPessoa/TPessoaDashboard";
export default function TPessoaDashboardPage() {
return (
<TPessoaDashboard />
);
}

View file

@ -0,0 +1,7 @@
import TServicoPedidoIndex from "@/packages/servicos/components/TServicoPedido/TServicoPedidoIndex";
export default function TServicoPedidoPage() {
return (
<TServicoPedidoIndex />
)
}

View file

@ -0,0 +1,90 @@
import { Card, CardContent } from "@/components/ui/card";
import TServicoItemPedidoResumo from "@/packages/servicos/components/TServicoItemPedido/TServicoItemPedidoResumo";
const itens = [
{
id: 1,
descricao: "Reconhecimento de Firma",
valor: 12.50,
},
{
id: 2,
descricao: "Autenticação de Cópia",
valor: 6.00,
},
{
id: 3,
descricao: "Procuração Pública",
valor: 98.75,
},
{
id: 4,
descricao: "Certidão de Escritura",
valor: 42.30,
},
{
id: 5,
descricao: "Registro de Documento",
valor: 73.10,
},
]
export default function PedidoPage() {
return (
<div>
<div className="grid w-full grid-cols-12 gap-4">
<div className="col-span-12 sm:col-span-9 md:col-span-9">
<Card>
<CardContent>
<div className="grid w-full grid-cols-12 gap-4">
<div className="col-span-12 sm:col-span-3 md:col-span-3">
<Card>
<CardContent>
</CardContent>
</Card>
</div>
<div className="col-span-12 sm:col-span-3 md:col-span-3">
<Card>
<CardContent>
</CardContent>
</Card>
</div>
<div className="col-span-12 sm:col-span-3 md:col-span-3">
<Card>
<CardContent>
</CardContent>
</Card>
</div>
<div className="col-span-12 sm:col-span-3 md:col-span-3">
<Card>
<CardContent>
</CardContent>
</Card>
</div>
<div className="col-span-12 sm:col-span-3 md:col-span-3">
<Card>
<CardContent>
</CardContent>
</Card>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="col-span-12 sm:col-span-3 md:col-span-3">
<TServicoItemPedidoResumo
dataPedido="01/01/2026"
numeroPedido={123}
itens={itens}
/>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,7 @@
import TServicoPedidoDashboard from "@/packages/servicos/components/TServicoPedido/TServicoPedidoDashboard";
export default function TServicoPedidoPage() {
return (
<TServicoPedidoDashboard />
)
}

View file

@ -1,15 +1,15 @@
'use client';
import * as React from 'react';
import {
Bot,
Frame,
GalleryVerticalEnd,
House,
HouseIcon,
SquareTerminal,
UsersIcon,
UsersIcon
} from 'lucide-react';
import Image from 'next/image';
import * as React from 'react';
import { NavMain } from '@/components/nav-main';
import { NavProjects } from '@/components/nav-projects';
@ -24,9 +24,8 @@ import {
SidebarMenuItem,
SidebarRail,
} from '@/components/ui/sidebar';
import useGUsuarioGetJWTHook from '@/shared/hooks/auth/useGUsuarioGetJWTHook';
import Image from 'next/image';
// This is sample data.
const data = {
@ -44,12 +43,32 @@ const data = {
},
],
},
{
title: 'Servicos',
url: '#',
icon: UsersIcon,
isActive: false,
items: [
{
title: 'Dashboard',
url: '/servicos/dashboard/',
},
{
title: 'Balcão',
url: '/servicos/balcao/',
},
],
},
{
title: 'Pessoas',
url: '#',
icon: UsersIcon,
isActive: false,
items: [
{
title: 'Dashboard',
url: '/administrativo/pessoas/dashboard',
},
{
title: 'Físicas',
url: '/administrativo/pessoas/fisicas',
@ -66,6 +85,10 @@ const data = {
icon: HouseIcon,
isActive: false,
items: [
{
title: 'Dashboard',
url: '/administrativo/imoveis/dashboard',
},
{
title: 'Urbanos',
url: '/administrativo/imoveis/urbanos',

View file

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View file

@ -1,36 +1,40 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
},
);
}
)
function Button({
className,
@ -38,11 +42,11 @@ function Button({
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button';
const Comp = asChild ? Slot : "button"
return (
<Comp
@ -50,7 +54,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
)
}
export { Button, buttonVariants };
export { Button, buttonVariants }

View file

@ -1,75 +1,92 @@
import * as React from 'react';
import * as React from "react"
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<'div'>) {
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className,
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
);
)
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
);
)
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn('leading-none font-semibold', className)}
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
)
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn('text-muted-foreground text-sm', className)}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
)
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
);
)
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="card-content" className={cn('px-6', className)} {...props} />;
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -1,22 +1,21 @@
import * as React from 'react';
import * as React from "react"
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils"
function Input({ className, value, type, ...props }: React.ComponentProps<'input'>) {
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className,
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
value={value ?? ''}
{...props}
/>
);
)
}
export { Input };
export { Input }

View file

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View file

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }

View file

@ -1,30 +1,36 @@
'use client';
"use client"
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from '@/lib/utils';
import { cn } from "@/lib/utils"
function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = 'default',
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: 'sm' | 'default';
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
@ -32,7 +38,7 @@ function SelectTrigger({
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
@ -41,13 +47,14 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
)
}
function SelectContent({
className,
children,
position = 'popper',
position = "popper",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
@ -55,20 +62,21 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
@ -76,17 +84,20 @@ function SelectContent({
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
)
}
function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
)
}
function SelectItem({
@ -99,7 +110,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
className
)}
{...props}
>
@ -110,7 +121,7 @@ function SelectItem({
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
)
}
function SelectSeparator({
@ -120,10 +131,10 @@ function SelectSeparator({
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
)
}
function SelectScrollUpButton({
@ -133,12 +144,15 @@ function SelectScrollUpButton({
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn('flex cursor-default items-center justify-center py-1', className)}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
)
}
function SelectScrollDownButton({
@ -148,12 +162,15 @@ function SelectScrollDownButton({
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn('flex cursor-default items-center justify-center py-1', className)}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
)
}
export {
@ -167,4 +184,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
};
}

View file

@ -0,0 +1,378 @@
'use client'
import { motion } from 'framer-motion'
import {
AlertTriangle,
Building2,
CheckCircle2,
Home,
Layers3,
MapPin,
Ruler
} from 'lucide-react'
import React, { useEffect, useState } from 'react'
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardHeader,
CardTitle
} from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
// =====================
// Types
// =====================
type Kpis = {
totalImoveis: number
totalUnidades: number
mediaPorImovel: number
geoReferenciados: number
semArea: number
novosMes: number
tendenciaMes: number
}
type AreaStats = {
media: number
minima: number
maxima: number
}
type Distribuicao = { label: string; total: number }
type Qualidade = {
geoReferenciadoPct: number
areaPreenchidaPct: number
enderecoCompletoPct: number
inconsistencias: number
}
type ApiResponse = {
kpis: Kpis
area: AreaStats
tiposClasse: Distribuicao[]
tiposRegistro: Distribuicao[]
distribuicaoUF: Distribuicao[]
distribuicaoCidade: Distribuicao[]
auditoria: Distribuicao[]
evolucao: { mes: string; total: number }[]
qualidade: Qualidade
}
// =====================
// Mock de fallback
// =====================
const MOCK: ApiResponse = {
kpis: {
totalImoveis: 1240,
totalUnidades: 8950,
mediaPorImovel: 7.2,
geoReferenciados: 5620,
semArea: 230,
novosMes: 110,
tendenciaMes: 5.8
},
area: {
media: 82.4,
minima: 30.5,
maxima: 250.0
},
tiposClasse: [
{ label: 'Urbano', total: 780 },
{ label: 'Rural', total: 420 },
{ label: 'Não informado', total: 40 }
],
tiposRegistro: [
{ label: 'Matrícula', total: 900 },
{ label: 'Transcrição', total: 280 },
{ label: 'Outros', total: 60 }
],
distribuicaoUF: [
{ label: 'GO', total: 540 },
{ label: 'DF', total: 230 },
{ label: 'MT', total: 150 },
{ label: 'TO', total: 120 }
],
distribuicaoCidade: [
{ label: 'Goiânia', total: 320 },
{ label: 'Anápolis', total: 220 },
{ label: 'Aparecida de Goiânia', total: 210 },
{ label: 'Trindade', total: 180 },
{ label: 'Formosa', total: 160 },
{ label: 'Catalão', total: 140 },
{ label: 'Luziânia', total: 120 },
{ label: 'Rio Verde', total: 110 },
{ label: 'Itumbiara', total: 90 },
{ label: 'Jataí', total: 80 }
],
auditoria: [
{ label: 'Unidades sem área', total: 230 },
{ label: 'Imóveis sem UF', total: 15 },
{ label: 'Sem logradouro', total: 48 }
],
evolucao: Array.from({ length: 12 }, (_, i) => ({
mes: new Date(2025, i).toLocaleString('pt-BR', { month: 'short' }),
total: 100 + i * 25 + (i % 2 === 0 ? 40 : 0)
})),
qualidade: {
geoReferenciadoPct: 72.5,
areaPreenchidaPct: 88.2,
enderecoCompletoPct: 93.4,
inconsistencias: 48
}
}
// =====================
// Função de fetch (com fallback)
// =====================
async function fetchApi(periodo: string, uf: string | null): Promise<ApiResponse> {
const params = new URLSearchParams()
params.set('periodo', periodo)
if (uf) params.set('uf', uf)
try {
const ctrl = new AbortController()
const timeout = setTimeout(() => ctrl.abort(), 6000)
const res = await fetch(`/api/dashboard/imoveis?${params.toString()}`, { signal: ctrl.signal })
clearTimeout(timeout)
if (!res.ok) throw new Error('Erro HTTP')
return await res.json()
} catch {
return MOCK
}
}
// =====================
// Subcomponentes
// =====================
function Kpi({
icon: Icon,
label,
value,
hint,
trend
}: {
icon: any
label: string
value: React.ReactNode
hint?: string
trend?: number
}) {
const trendColor =
trend == null ? '' : trend > 0 ? 'text-emerald-600' : trend < 0 ? 'text-rose-600' : 'text-muted-foreground'
const prefix = trend != null && trend > 0 ? '+' : ''
return (
<Card className="shadow-sm border-muted/40">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm text-muted-foreground">{label}</CardTitle>
<Icon className="h-5 w-5 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground mt-1 flex gap-2">
{hint && <span>{hint}</span>}
{trend != null && (
<span className={`font-medium ${trendColor}`}>{prefix}{trend.toFixed(1)}%</span>
)}
</p>
</CardContent>
</Card>
)
}
function DataProgress({ label, value, goal }: { label: string; value: number; goal: number }) {
const color = value >= goal ? 'text-emerald-600' : 'text-amber-600'
return (
<div className="space-y-1">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{label}</span>
<span className={`font-medium ${color}`}>
{value.toFixed(1)}% <span className="text-muted-foreground">(meta {goal}%)</span>
</span>
</div>
<Progress value={Math.min(100, value)} />
</div>
)
}
// =====================
// Main Component
// =====================
export default function DashboardImoveis() {
const [periodo, setPeriodo] = useState('12m')
const [uf, setUf] = useState<string | null>(null)
const [data, setData] = useState<ApiResponse | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
fetchApi(periodo, uf).then((d) => {
setData(d)
setLoading(false)
})
}, [periodo, uf])
const COLORS = ['#1A292F', '#8FB6C1', '#D1E6EA', '#F36F28', '#EAECEA']
return (
<div className="p-4 md:p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<motion.h1 initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} className="text-2xl font-semibold tracking-tight">
Painel de Imóveis e Unidades
</motion.h1>
<p className="text-sm text-muted-foreground">Gestão cadastral, georreferenciamento e auditoria</p>
</div>
<div className="flex items-center gap-2">
<Select value={periodo} onValueChange={(v) => setPeriodo(v)}>
<SelectTrigger className="w-[140px]"><SelectValue placeholder="Período" /></SelectTrigger>
<SelectContent>
<SelectItem value="6m">Últimos 6 meses</SelectItem>
<SelectItem value="12m">Últimos 12 meses</SelectItem>
<SelectItem value="24m">Últimos 24 meses</SelectItem>
</SelectContent>
</Select>
<Select value={uf ?? '__all__'} onValueChange={(v) => setUf(v === '__all__' ? null : v)}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="UF" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Todas UFs</SelectItem>
{(data?.distribuicaoUF ?? []).map((u) => (
<SelectItem key={u.label} value={u.label}>
{u.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" onClick={() => setUf(null)}>Limpar</Button>
</div>
</div>
{/* KPIs */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-4">
<Kpi icon={Home} label="Imóveis" value={data?.kpis.totalImoveis ?? 0} hint={`${data?.kpis.novosMes ?? 0} novos`} trend={data?.kpis.tendenciaMes} />
<Kpi icon={Building2} label="Unidades" value={data?.kpis.totalUnidades ?? 0} />
<Kpi icon={Layers3} label="Média por Imóvel" value={data?.kpis.mediaPorImovel.toFixed(1)} />
<Kpi icon={MapPin} label="GeoRef." value={`${data?.qualidade.geoReferenciadoPct ?? 0}%`} />
<Kpi icon={AlertTriangle} label="Sem área" value={data?.kpis.semArea ?? 0} />
</div>
{/* Evolução */}
<Card>
<CardHeader><CardTitle>Evolução de Cadastros</CardTitle></CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data?.evolucao ?? []}>
<defs>
<linearGradient id="gradImovel" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#F36F28" stopOpacity={0.35} />
<stop offset="95%" stopColor="#F36F28" stopOpacity={0.05} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="mes" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="total" stroke="#F36F28" fill="url(#gradImovel)" strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Distribuição por UF e Cidades */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader><CardTitle>Imóveis por UF</CardTitle></CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.distribuicaoUF ?? []}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="label" />
<YAxis />
<Tooltip />
<Bar dataKey="total" radius={[6, 6, 0, 0]}>
{(data?.distribuicaoUF ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>Top 10 Cidades</CardTitle></CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.distribuicaoCidade ?? []} layout="vertical">
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis type="number" />
<YAxis type="category" dataKey="label" width={120} />
<Tooltip />
<Bar dataKey="total" radius={[0, 6, 6, 0]}>
{(data?.distribuicaoCidade ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[(i + 3) % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Qualidade de Dados */}
<Card>
<CardHeader><CardTitle>Qualidade de Dados</CardTitle></CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-3">
<DataProgress label="GeoReferenciados" value={data?.qualidade.geoReferenciadoPct ?? 0} goal={80} />
<DataProgress label="Área preenchida" value={data?.qualidade.areaPreenchidaPct ?? 0} goal={90} />
<DataProgress label="Endereço completo" value={data?.qualidade.enderecoCompletoPct ?? 0} goal={90} />
</div>
<div className="flex flex-col justify-center items-center">
<Ruler className="h-10 w-10 text-[#8FB6C1] mb-2" />
<p className="text-sm text-muted-foreground text-center">Área média cadastrada</p>
<p className="text-2xl font-semibold text-[#1A292F] dark:text-white">{data?.area.media.toFixed(1)} m²</p>
</div>
<div className="flex flex-col items-center justify-center space-y-1">
<CheckCircle2 className="h-8 w-8 text-emerald-600" />
<Badge variant="outline" className="text-sm font-medium">
{data?.qualidade.inconsistencias ?? 0} inconsistências
</Badge>
</div>
</CardContent>
</Card>
<p className="text-xs text-muted-foreground text-center pt-4">
Fonte: VIEW VW_T_IMOVEL_ANALYTICS fallback automático para mock.
</p>
</div>
)
}

View file

@ -0,0 +1,506 @@
'use client'
import { motion } from "framer-motion";
import { Building2, CalendarClock, Loader2, Mail, Users } from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
// =====================
// Types
// =====================
type SexoDistribuicao = { genero: string; total: number };
type EvolucaoCadastro = { ano: number; mes: number; total: number };
type CategoriaValor = { label: string; total: number };
type Kpis = {
total: number;
fisicas: number;
juridicas: number;
mediaIdade: number | null;
pctCpfCnpjValidos: number;
pctEmailPreenchido: number;
novosMes: number;
tendenciaMes: number; // variação % em relação ao mês anterior
};
type Qualidade = {
email: number;
enderecoCompleto: number;
telefoneValido: number;
cpfInvalido: number; // quantidade
};
type ApiResponse = {
kpis: Kpis;
sexo: SexoDistribuicao[];
evolucao: EvolucaoCadastro[];
estadoCivil: CategoriaValor[];
faixasEtarias: CategoriaValor[];
profissoesTop10: CategoriaValor[];
ufDistribuicao: CategoriaValor[];
cidadesTop10: CategoriaValor[];
qualidade: Qualidade;
};
// =====================
// Mock (fallback)
// =====================
const MOCK: ApiResponse = {
kpis: {
total: 12876,
fisicas: 11234,
juridicas: 1642,
mediaIdade: 39,
pctCpfCnpjValidos: 87.4,
pctEmailPreenchido: 91.2,
novosMes: 328,
tendenciaMes: 6.3,
},
sexo: [
{ genero: "Masculino", total: 5840 },
{ genero: "Feminino", total: 5210 },
{ genero: "Não informado", total: 184 }
],
evolucao: Array.from({ length: 12 }, (_, i) => ({ ano: 2025, mes: i + 1, total: 120 + i * 15 + (i % 2 === 0 ? 30 : 0) })),
estadoCivil: [
{ label: "Solteiro(a)", total: 4312 },
{ label: "Casado(a)", total: 5120 },
{ label: "Divorciado(a)", total: 812 },
{ label: "Viúvo(a)", total: 210 },
],
faixasEtarias: [
{ label: "<18", total: 420 },
{ label: "18-30", total: 3420 },
{ label: "31-45", total: 5180 },
{ label: "46-60", total: 2780 },
{ label: "60+", total: 1076 },
],
profissoesTop10: [
{ label: "Estudante", total: 780 },
{ label: "Comerciante", total: 620 },
{ label: "Professor(a)", total: 570 },
{ label: "Autônomo(a)", total: 510 },
{ label: "Servidor(a) Público(a)", total: 450 },
{ label: "Engenheiro(a)", total: 430 },
{ label: "Advogado(a)", total: 410 },
{ label: "Médico(a)", total: 385 },
{ label: "Enfermeiro(a)", total: 360 },
{ label: "Agricultor(a)", total: 350 },
],
ufDistribuicao: [
{ label: "GO", total: 5080 },
{ label: "DF", total: 1840 },
{ label: "MG", total: 980 },
{ label: "SP", total: 820 },
{ label: "BA", total: 740 },
],
cidadesTop10: [
{ label: "Goiânia", total: 2200 },
{ label: "Aparecida de Goiânia", total: 980 },
{ label: "Anápolis", total: 640 },
{ label: "Formosa", total: 510 },
{ label: "Trindade", total: 480 },
{ label: "Luziânia", total: 465 },
{ label: "Catalão", total: 430 },
{ label: "Rio Verde", total: 415 },
{ label: "Itumbiara", total: 400 },
{ label: "Jataí", total: 395 },
],
qualidade: {
email: 91.2,
enderecoCompleto: 83.4,
telefoneValido: 77.9,
cpfInvalido: 248,
},
};
// =====================
// Helpers
// =====================
const monthName = (m: number) => new Date(2025, m - 1, 1).toLocaleDateString("pt-BR", { month: "short" }).replace(".", "");
async function fetchApi(periodo: string, uf: string | null, cidade: string | null): Promise<ApiResponse> {
const params = new URLSearchParams();
params.set("periodo", periodo); // ex: "12m", "24m", "ytd"
if (uf) params.set("uf", uf);
if (cidade) params.set("cidade", cidade);
try {
const ctrl = new AbortController();
const to = setTimeout(() => ctrl.abort(), 6000);
const res = await fetch(`/api/dashboard/pessoas?${params.toString()}`, { signal: ctrl.signal });
clearTimeout(to);
if (!res.ok) throw new Error("HTTP error");
const data = (await res.json()) as ApiResponse;
return data;
} catch {
// fallback para mock
return MOCK;
}
}
// =====================
// Subcomponents
// =====================
function Kpi({ icon: Icon, label, value, hint, trend }: { icon: any; label: string; value: React.ReactNode; hint?: string; trend?: number }) {
const trendColor = trend == null ? "" : trend > 0 ? "text-emerald-600" : trend < 0 ? "text-rose-600" : "text-muted-foreground";
const trendPrefix = trend == null ? "" : trend > 0 ? "+" : "";
return (
<Card className="shadow-sm border-muted/40">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
<Icon className="h-5 w-5 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground mt-1 flex items-center gap-2">
{hint && <span>{hint}</span>}
{trend != null && (
<span className={`font-medium ${trendColor}`}>{trendPrefix}{trend.toFixed(1)}%</span>
)}
</p>
</CardContent>
</Card>
);
}
function Section({ title, description, children, right = null }: { title: string; description?: string; children: React.ReactNode; right?: React.ReactNode | null }) {
return (
<section className="space-y-3">
<div className="flex items-end justify-between gap-4">
<div>
<h3 className="text-base font-semibold">{title}</h3>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
{right}
</div>
<div className="grid grid-cols-1">{children}</div>
</section>
);
}
function DataProgress({ label, value, goal }: { label: string; value: number; goal: number }) {
const color = value >= goal ? "text-emerald-600" : "text-amber-600";
return (
<div className="space-y-1">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{label}</span>
<span className={`font-medium ${color}`}>{value.toFixed(1)}% <span className="text-muted-foreground">(meta {goal}%)</span></span>
</div>
<Progress value={Math.min(100, value)} />
</div>
);
}
// =====================
// Main Component
// =====================
export default function TPessoaDashboard() {
const [periodo, setPeriodo] = useState("12m");
const [uf, setUf] = useState<string | null>(null);
const [cidade, setCidade] = useState<string | null>(null);
const [data, setData] = useState<ApiResponse | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchApi(periodo, uf, cidade).then((d) => {
setData(d);
setLoading(false);
});
}, [periodo, uf, cidade]);
const evolucaoFmt = useMemo(() => {
return (data?.evolucao ?? []).map((d) => ({
mes: `${monthName(d.mes)}/${String(d.ano).slice(2)}`,
total: d.total,
}));
}, [data]);
const sexoFmt = useMemo(() => (data?.sexo ?? []).map((s) => ({ name: s.genero, value: s.total })), [data]);
const COLORS = ["#0ea5e9", "#22c55e", "#a3a3a3", "#a78bfa", "#fb7185", "#fbbf24"]; // não fixa cores globais, apenas define fallback estável
return (
<div className="p-4 md:p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<motion.h1 initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} className="text-2xl font-semibold tracking-tight">
Painel de Pessoas
</motion.h1>
<p className="text-sm text-muted-foreground">Análise demográfica, documental e cadastral</p>
</div>
<div className="flex items-center gap-2">
<Select value={periodo} onValueChange={(v) => setPeriodo(v)}>
<SelectTrigger className="w-[140px]"><SelectValue placeholder="Período" /></SelectTrigger>
<SelectContent>
<SelectItem value="6m">Últimos 6 meses</SelectItem>
<SelectItem value="12m">Últimos 12 meses</SelectItem>
<SelectItem value="24m">Últimos 24 meses</SelectItem>
<SelectItem value="ytd">Ano atual (YTD)</SelectItem>
</SelectContent>
</Select>
<Select value={uf ?? "all"} onValueChange={(v) => setUf(v === "all" ? null : v)}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="UF" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas UFs</SelectItem>
{(data?.ufDistribuicao ?? []).map((u) => (
<SelectItem key={u.label} value={u.label}>{u.label}</SelectItem>
))}
</SelectContent>
</Select>
<Input placeholder="Filtrar cidade" className="w-[200px]" onChange={(e) => setCidade(e.target.value || null)} />
<Button variant="outline" onClick={() => { setUf(null); setCidade(null); }}>Limpar</Button>
</div>
</div>
{/* KPIs */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
<Kpi icon={Users} label="Total de Pessoas" value={loading ? <Loader2 className="h-5 w-5 animate-spin" /> : data?.kpis.total.toLocaleString("pt-BR")} hint={`${data?.kpis.novosMes ?? 0} novos no mês`} trend={data?.kpis.tendenciaMes} />
<Kpi icon={CalendarClock} label="Média de idade" value={loading ? <Loader2 className="h-5 w-5 animate-spin" /> : (data?.kpis.mediaIdade ?? 0)} hint="anos" />
<Kpi icon={Building2} label="% CPF/CNPJ válidos" value={loading ? <Loader2 className="h-5 w-5 animate-spin" /> : `${data?.kpis.pctCpfCnpjValidos.toFixed(1)}%`} />
<Kpi icon={Mail} label="% E-mail preenchido" value={loading ? <Loader2 className="h-5 w-5 animate-spin" /> : `${data?.kpis.pctEmailPreenchido.toFixed(1)}%`} />
</div>
{/* Evolução & Sexo */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Evolução de Cadastros</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={evolucaoFmt} margin={{ left: 8, right: 8, top: 10, bottom: 0 }}>
<defs>
<linearGradient id="grad1" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#0ea5e9" stopOpacity={0.35} />
<stop offset="95%" stopColor="#0ea5e9" stopOpacity={0.01} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="mes" interval={1} tickMargin={8} />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="total" stroke="#0ea5e9" fill="url(#grad1)" strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Distribuição por Sexo</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Tooltip />
<Legend />
<Pie data={sexoFmt} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={90} label>
{sexoFmt.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Estado Civil & Faixa etária */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Estado Civil</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.estadoCivil ?? []} margin={{ left: 8, right: 8, top: 10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="label" tickMargin={8} />
<YAxis />
<Tooltip />
<Bar dataKey="total" radius={[6, 6, 0, 0]}>
{(data?.estadoCivil ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Faixas Etárias</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.faixasEtarias ?? []} margin={{ left: 8, right: 8, top: 10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="label" tickMargin={8} />
<YAxis />
<Tooltip />
<Bar dataKey="total" radius={[6, 6, 0, 0]}>
{(data?.faixasEtarias ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[(i + 2) % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Geografia */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Distribuição por UF</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.ufDistribuicao ?? []} margin={{ left: 8, right: 8, top: 10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="label" tickMargin={8} />
<YAxis />
<Tooltip />
<Bar dataKey="total" radius={[6, 6, 0, 0]}>
{(data?.ufDistribuicao ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[(i + 3) % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Top 10 Cidades</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.cidadesTop10 ?? []} layout="vertical" margin={{ left: 24, right: 8, top: 10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis type="number" />
<YAxis type="category" dataKey="label" width={120} />
<Tooltip />
<Bar dataKey="total" radius={[0, 6, 6, 0]}>
{(data?.cidadesTop10 ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[(i + 4) % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Profissões */}
<Section title="Top 10 Profissões" description="Profissões mais frequentes entre os registros">
<Card>
<CardContent className="h-[320px] pt-6">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.profissoesTop10 ?? []} margin={{ left: 8, right: 8, top: 10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="label" tickMargin={8} angle={-20} height={60} textAnchor="end" />
<YAxis />
<Tooltip />
<Bar dataKey="total" radius={[6, 6, 0, 0]}>
{(data?.profissoesTop10 ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[(i + 5) % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Section>
{/* Qualidade de Dados */}
<Section title="Qualidade de Dados" description="Cobertura de campos críticos e inconsistências">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-4"><CardTitle>Cobertura</CardTitle></CardHeader>
<CardContent className="space-y-3">
<DataProgress label="E-mail preenchido" value={data?.qualidade.email ?? 0} goal={90} />
<DataProgress label="Endereço completo" value={data?.qualidade.enderecoCompleto ?? 0} goal={85} />
<DataProgress label="Telefone válido" value={data?.qualidade.telefoneValido ?? 0} goal={80} />
</CardContent>
</Card>
<Card>
<CardHeader className="pb-4"><CardTitle>Indicadores</CardTitle></CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">% CPF/CNPJ válidos</span>
<Badge variant="outline">{data?.kpis.pctCpfCnpjValidos.toFixed(1)}%</Badge>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">% E-mail preenchido</span>
<Badge variant="outline">{data?.kpis.pctEmailPreenchido.toFixed(1)}%</Badge>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Registros com CPF inconsistente</span>
<Badge variant="destructive" className="font-mono">{data?.qualidade.cpfInvalido.toLocaleString("pt-BR")}</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-4"><CardTitle>Ações rápidas</CardTitle></CardHeader>
<CardContent className="space-y-2">
<Button variant="secondary" className="w-full" onClick={() => alert("Exportar inconsistências (CSV)")}>Exportar inconsistências</Button>
<Button variant="outline" className="w-full" onClick={() => alert("Abrir relatório detalhado")}>Abrir relatório detalhado</Button>
<Button className="w-full" onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}>Voltar ao topo</Button>
</CardContent>
</Card>
</div>
</Section>
<p className="text-xs text-muted-foreground text-center pt-4">Fonte de dados: API /api/dashboard/pessoas fallback automático para mock se indisponível.</p>
</div>
);
}

View file

@ -0,0 +1,78 @@
import { ColumnDef } from '@tanstack/react-table';
import { EllipsisIcon, PencilIcon, Trash2Icon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
import { SortableHeader } from '@/shared/components/dataTable/SortableHeader';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
export default function TServicoItemPedidoColumns(
onEdit: (item: TServicoItemPedidoInterface, isEditingFormStatus: boolean) => void,
onDelete: (item: TServicoItemPedidoInterface, isEditingFormStatus: boolean) => void,
): ColumnDef<TServicoItemPedidoInterface>[] {
return [
// ID
{
accessorKey: 'gramatica_id',
header: ({ column }) => SortableHeader('ID', column),
cell: ({ row }) => Number(row.getValue('gramatica_id')),
enableSorting: true,
},
// Descrição
{
accessorKey: 'palavra',
header: ({ column }) => SortableHeader('Palavra', column),
cell: ({ row }) => GetCapitalize(String(row.getValue('palavra') || '')),
},
// Ações
{
id: 'actions',
header: 'Ações',
cell: ({ row }) => {
const natureza = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="left" align="start">
<DropdownMenuGroup>
<DropdownMenuItem onSelect={() => onEdit(natureza, true)}>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onSelect={() => onDelete(natureza, true)}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
},
enableSorting: false,
enableHiding: false,
},
];
}

View file

@ -0,0 +1,205 @@
'use client';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { ResetFormIfData } from '@/shared/actions/form/ResetFormIfData';
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
import { useTServicoItemPedidoFormHook } from '../../hooks/TServicoItemPedido/useTServicoItemPedidoFormHook';
import { TServicoItemPedidoFormInterface } from '../../interfaces/TServicoItemPedido/TServicoItemPedidoFormInterface';
/**
* Formulário de cadastro/edição de Natureza
* Baseado nos campos da tabela G_NATUREZA
*/
export default function TServicoItemPedidoForm({
isOpen,
data,
onClose,
onSave,
buttonIsLoading,
}: TServicoItemPedidoFormInterface) {
const form = useTServicoItemPedidoFormHook({});
// Atualiza o formulário quando recebe dados para edição
useEffect(() => {
ResetFormIfData(form, data);
}, [data, form]);
function onError(error: any) {
console.log('Erro no formulário:', error);
}
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose(null, false);
}}
>
<DialogContent className="w-full max-w-full p-6 sm:max-w-3xl md:max-w-2xl lg:max-w-2xl">
<DialogHeader>
<DialogTitle className="text-lg sm:text-xl">Formulário de Gramática</DialogTitle>
<DialogDescription className="text-muted-foreground text-sm">
Formulário de Gramática
</DialogDescription>
</DialogHeader>
{/* Formulário principal */}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSave, onError)} className="space-y-6">
{/* GRID MOBILE FIRST */}
<div className="grid w-full grid-cols-12 gap-4">
{/* Palavra */}
<div className="col-span-12 sm:col-span-6 md:col-span-12">
<FormField
control={form.control}
name="palavra"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Palavra</FormLabel>
<FormControl>
<Input
{...field}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Prefixo */}
<div className="col-span-12 sm:col-span-6 md:col-span-12">
<FormField
control={form.control}
name="prefixo"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Prefixo</FormLabel>
<FormControl>
<Input
{...field}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Singular Masculino */}
<div className="col-span-12 sm:col-span-6 md:col-span-6">
<FormField
control={form.control}
name="sufixo_ms"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Sufixo Masculino Singular</FormLabel>
<FormControl>
<Input
{...field}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Plural Masculino */}
<div className="col-span-12 sm:col-span-6 md:col-span-6">
<FormField
control={form.control}
name="sufixo_mp"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Sufixo Masculino Plural</FormLabel>
<FormControl>
<Input
{...field}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Singular Feminino */}
<div className="col-span-12 sm:col-span-6 md:col-span-6">
<FormField
control={form.control}
name="sufixo_fs"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Sufixo Feminino Singular</FormLabel>
<FormControl>
<Input
{...field}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Plural Feminino */}
<div className="col-span-12 sm:col-span-6 md:col-span-6">
<FormField
control={form.control}
name="sufixo_fp"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Sufixo Feminino Plural</FormLabel>
<FormControl>
<Input
{...field}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Rodapé */}
<DialogFooter className="mt-6 flex flex-col justify-end gap-2 sm:flex-row">
<DialogClose asChild>
<Button variant="outline" type="button">
Cancelar
</Button>
</DialogClose>
<LoadingButton
text="Salvar"
textLoading="Salvando..."
type="submit"
loading={buttonIsLoading}
/>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,161 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useTServicoItemPedidoDeleteHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoDeleteHook';
import { useTServicoItemPedidoIndexHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoIndexHook';
import { useTServicoItemPedidoSaveHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoSaveHook';
import TServicoItemPedidoInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
import ConfirmDialog from '@/shared/components/confirmDialog/ConfirmDialog';
import { useConfirmDialog } from '@/shared/components/confirmDialog/useConfirmDialog';
import Loading from '@/shared/components/loading/loading';
import Header from '@/shared/components/structure/Header';
import TServicoItemPedidoForm from './TServicoItemPedidoForm';
import TServicoItemPedidoTable from './TServicoItemPedidoTable';
export default function TServicoItemPedidoIndex() {
// Controle de estado do botão
const [buttonIsLoading, setButtonIsLoading] = useState(false);
// Hooks para leitura e salvamento
const { TServicoItemPedido, indexTServicoItemPedido } = useTServicoItemPedidoIndexHook();
const { saveTServicoItemPedido } = useTServicoItemPedidoSaveHook();
const { deleteTServicoItemPedido } = useTServicoItemPedidoDeleteHook();
// Estados
const [selectedData, setSelectedData] = useState<TServicoItemPedidoInterface | null>(null);
const [isFormOpen, setIsFormOpen] = useState(false);
// Estado para saber qual item será deletado
const [itemToDelete, setItemToDelete] = useState<TServicoItemPedidoInterface | null>(null);
/**
* Hook do modal de confirmação
*/
const { isOpen: isConfirmOpen, openDialog: openConfirmDialog, handleCancel } = useConfirmDialog();
/**
* Abre o formulário no modo de edição ou criação
*/
const handleOpenForm = useCallback((data: TServicoItemPedidoInterface | null) => {
setSelectedData(data);
setIsFormOpen(true);
}, []);
/**
* Fecha o formulário e limpa o andamento selecionado
*/
const handleCloseForm = useCallback(() => {
setSelectedData(null);
setIsFormOpen(false);
}, []);
/**
* Salva os dados do formulário
*/
const handleSave = useCallback(
async (formData: TServicoItemPedidoInterface) => {
// Coloca o botão em estado de loading
setButtonIsLoading(true);
// Aguarda salvar o registro
await saveTServicoItemPedido(formData);
// Remove o botão em estado de loading
setButtonIsLoading(false);
// Atualiza a lista de dados
indexTServicoItemPedido();
},
[saveTServicoItemPedido, indexTServicoItemPedido, handleCloseForm],
);
/**
* Quando o usuário clica em "remover" na tabela
*/
const handleConfirmDelete = useCallback(
(item: TServicoItemPedidoInterface) => {
// Define o item atual para remoção
setItemToDelete(item);
// Abre o modal de confirmação
openConfirmDialog();
},
[openConfirmDialog],
);
/**
* Executa a exclusão de fato quando o usuário confirma
*/
const handleDelete = useCallback(async () => {
// Protege contra null
if (!itemToDelete) return;
// Executa o Hook de remoção
await deleteTServicoItemPedido(itemToDelete);
// Atualiza a lista
await indexTServicoItemPedido();
// Limpa o item selecionado
setItemToDelete(null);
// Fecha o modal
handleCancel();
}, [itemToDelete, indexTServicoItemPedido, handleCancel]);
/**
* Busca inicial dos dados
*/
useEffect(() => {
indexTServicoItemPedido();
}, []);
/**
* Tela de loading enquanto carrega os dados
*/
if (TServicoItemPedido?.length == 0) {
return <Loading type={2} />;
}
return (
<div>
{/* Cabeçalho */}
<Header
title={'Gramatica'}
description={'Gramatica'}
buttonText={'Nova palavra'}
buttonAction={() => {
handleOpenForm(null);
}}
/>
{/* Tabela de andamentos */}
<TServicoItemPedidoTable data={TServicoItemPedido} onEdit={handleOpenForm} onDelete={handleConfirmDelete} />
{/* Modal de confirmação */}
{isConfirmOpen && (
<ConfirmDialog
isOpen={isConfirmOpen}
title="Confirmar exclusão"
description="Atenção"
message={`Deseja realmente excluir o valor "${itemToDelete?.valor}"?`}
confirmText="Sim, excluir"
cancelText="Cancelar"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
)}
{/* Formulário de criação/edição */}
{isFormOpen && (
<TServicoItemPedidoForm
isOpen={isFormOpen}
data={selectedData}
onClose={handleCloseForm}
onSave={handleSave}
buttonIsLoading={buttonIsLoading}
/>
)}
</div>
);
}

View file

@ -0,0 +1,92 @@
import { format } from "date-fns"
import { ptBR } from "date-fns/locale"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
interface PedidoItem {
id: number
descricao: string
valor: number
}
interface PedidoResumoProps {
numeroPedido: number
dataPedido: string
itens: PedidoItem[]
}
export default function TServicoItemPedidoResumo({
numeroPedido,
dataPedido,
itens,
}: PedidoResumoProps) {
const total = itens.reduce((acc, item) => acc + item.valor, 0)
return (
<Card
className="
w-full max-w-md
h-[380px] sm:h-[420px]
flex flex-col
bg-white/80 backdrop-blur-md
border border-[#D1E6EA]/50
shadow-md
rounded-xl
"
>
<CardHeader className="pb-3 border-b border-[#EAECEA]">
<CardTitle
className="
text-[#1A292F] text-base sm:text-sm
font-semibold flex justify-between items-center flex-wrap gap-2
"
>
<span>Pedido {numeroPedido}</span>
<Badge className="bg-[#F36F28]/10 text-[#F36F28] font-medium text-xs sm:text-sm">
{format(new Date(dataPedido), "dd/MM/yyyy HH:mm", { locale: ptBR })}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-3 sm:p-4 overflow-hidden">
<ScrollArea className="h-full pr-2 sm:pr-3">
{itens.map((item) => (
<div
key={item.id}
className="
flex justify-between items-center
py-2 text-sm sm:text-base
border-b border-[#EAECEA]/40 last:border-0
"
>
<span className="text-[#1A292F]/80 truncate max-w-[60%] sm:max-w-[70%]">
{item.descricao}
</span>
<span className="text-[#1A292F] font-medium">
R$ {item.valor.toFixed(2).replace(".", ",")}
</span>
</div>
))}
</ScrollArea>
</CardContent>
<Separator className="my-1" />
<CardFooter
className="
flex justify-between items-center
bg-[#1A292F]/5 py-3 px-3 sm:px-4
rounded-b-xl text-sm sm:text-base
"
>
<span className="text-[#1A292F] font-semibold">Total</span>
<span className="text-[#F36F28] font-bold text-lg sm:text-xl">
R$ {total.toFixed(2).replace(".", ",")}
</span>
</CardFooter>
</Card>
)
}

View file

@ -0,0 +1,23 @@
'use client';
import { DataTable } from '@/shared/components/dataTable/DataTable';
import TServicoItemPedidoTableInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoTableInterface';
import TServicoItemPedidoColumns from './TServicoItemPedidoColumns';
/**
* Componente principal da tabela de Naturezas
*/
export default function TServicoItemPedidoTable({ data, onEdit, onDelete }: TServicoItemPedidoTableInterface) {
const columns = TServicoItemPedidoColumns(onEdit, onDelete);
return (
<div>
<DataTable
data={data}
columns={columns}
filterColumn="palavra"
filterPlaceholder="Buscar por descrição da natureza..."
/>
</div>
);
}

View file

@ -0,0 +1,78 @@
import { ColumnDef } from '@tanstack/react-table';
import { EllipsisIcon, PencilIcon, Trash2Icon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
import { SortableHeader } from '@/shared/components/dataTable/SortableHeader';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
export default function TServicoPedidoColumns(
onEdit: (item: TServicoPedidoInterface, isEditingFormStatus: boolean) => void,
onDelete: (item: TServicoPedidoInterface, isEditingFormStatus: boolean) => void,
): ColumnDef<TServicoPedidoInterface>[] {
return [
// ID
{
accessorKey: 'gramatica_id',
header: ({ column }) => SortableHeader('ID', column),
cell: ({ row }) => Number(row.getValue('gramatica_id')),
enableSorting: true,
},
// Descrição
{
accessorKey: 'palavra',
header: ({ column }) => SortableHeader('Palavra', column),
cell: ({ row }) => GetCapitalize(String(row.getValue('palavra') || '')),
},
// Ações
{
id: 'actions',
header: 'Ações',
cell: ({ row }) => {
const natureza = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="left" align="start">
<DropdownMenuGroup>
<DropdownMenuItem onSelect={() => onEdit(natureza, true)}>
<PencilIcon className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onSelect={() => onDelete(natureza, true)}
>
<Trash2Icon className="mr-2 h-4 w-4" />
Remover
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
},
enableSorting: false,
enableHiding: false,
},
];
}

View file

@ -0,0 +1,318 @@
'use client'
import { motion } from 'framer-motion'
import {
Activity,
AlertTriangle,
DollarSign,
FileText, Layers,
Loader2
} from 'lucide-react'
import { useEffect, useState } from 'react'
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
Line,
LineChart,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis, YAxis
} from 'recharts'
import { Badge } from '@/components/ui/badge'
import {
Card,
CardContent,
CardHeader,
CardTitle
} from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
// ========================
// Tipos
// ========================
type Kpis = {
totalPedidos: number
totalItens: number
valorTotalPedidos: number
valorTotalPago: number
mediaDiferenca: number
}
type Evolucao = { mes: string; totalPedidos: number; somaPedidos: number }
type Situacao = { status: string; total: number }
type ServicoTipo = { servico_tipo: string; total: number; valor_total: number }
type Receita = { tipo: string; valor: number }
type Produtividade = { escrevente: string; totalPedidos: number; valorTotal: number }
type Qualidade = { itensSemValor: number; pedidosSemData: number; itensSemTipo: number }
type DashboardResponse = {
kpis: Kpis
evolucao: Evolucao[]
situacoes: Situacao[]
tiposServico: ServicoTipo[]
receitas: Receita[]
produtividade: Produtividade[]
certidoesPorMes: { mes: string; total: number }[]
qualidade: Qualidade
}
// ========================
// Mock (fallback)
// ========================
const MOCK: DashboardResponse = {
kpis: {
totalPedidos: 1845,
totalItens: 5630,
valorTotalPedidos: 425000,
valorTotalPago: 413200,
mediaDiferenca: 6.3,
},
evolucao: Array.from({ length: 12 }, (_, i) => ({
mes: new Date(2025, i).toLocaleString('pt-BR', { month: 'short' }),
totalPedidos: 100 + i * 20,
somaPedidos: 25000 + i * 3000
})),
situacoes: [
{ status: 'Ativo', total: 1240 },
{ status: 'Finalizado', total: 480 },
{ status: 'Cancelado', total: 125 },
],
tiposServico: [
{ servico_tipo: 'Certidão', total: 3100, valor_total: 135000 },
{ servico_tipo: 'Registro', total: 1800, valor_total: 98000 },
{ servico_tipo: 'Averbação', total: 730, valor_total: 46000 },
{ servico_tipo: 'Outros', total: 200, valor_total: 12000 },
],
receitas: [
{ tipo: 'Emolumentos', valor: 135000 },
{ tipo: 'Fundesp', valor: 28000 },
{ tipo: 'Taxa Judiciária', valor: 8000 },
{ tipo: 'ISS', valor: 12500 },
],
produtividade: [
{ escrevente: 'João Silva', totalPedidos: 420, valorTotal: 82000 },
{ escrevente: 'Maria Souza', totalPedidos: 380, valorTotal: 76000 },
{ escrevente: 'Carlos Lima', totalPedidos: 300, valorTotal: 69000 },
],
certidoesPorMes: Array.from({ length: 12 }, (_, i) => ({
mes: new Date(2025, i).toLocaleString('pt-BR', { month: 'short' }),
total: 60 + i * 10
})),
qualidade: {
itensSemValor: 14,
pedidosSemData: 9,
itensSemTipo: 6
}
}
// ========================
// Helper (mock fetch)
// ========================
async function fetchDashboard(): Promise<DashboardResponse> {
try {
const res = await fetch('/api/dashboard/servicos')
if (!res.ok) throw new Error('Erro HTTP')
return await res.json()
} catch {
return MOCK
}
}
// ========================
// Componente principal
// ========================
export default function TServicoPedidoDashboard() {
const [periodo, setPeriodo] = useState('12m')
const [data, setData] = useState<DashboardResponse | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
fetchDashboard().then((d) => {
setData(d)
setLoading(false)
})
}, [periodo])
const COLORS = ['#1A292F', '#8FB6C1', '#D1E6EA', '#F36F28', '#EAECEA']
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<motion.h1 initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} className="text-2xl font-semibold tracking-tight">
Dashboard Serviços e Pedidos
</motion.h1>
<p className="text-sm text-muted-foreground">Análise operacional, financeira e produtiva</p>
</div>
<Select value={periodo} onValueChange={(v) => setPeriodo(v)}>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="Período" />
</SelectTrigger>
<SelectContent>
<SelectItem value="6m">Últimos 6 meses</SelectItem>
<SelectItem value="12m">Últimos 12 meses</SelectItem>
<SelectItem value="24m">Últimos 24 meses</SelectItem>
</SelectContent>
</Select>
</div>
{/* KPIs */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-4">
<Kpi icon={FileText} label="Pedidos" value={data?.kpis.totalPedidos} />
<Kpi icon={Layers} label="Itens" value={data?.kpis.totalItens} />
<Kpi icon={DollarSign} label="Valor Total" value={formatCurrency(data?.kpis.valorTotalPedidos)} />
<Kpi icon={Activity} label="Valor Pago" value={formatCurrency(data?.kpis.valorTotalPago)} />
<Kpi icon={AlertTriangle} label="Dif. Média" value={`R$ ${data?.kpis.mediaDiferenca.toFixed(2)}`} />
</div>
{/* Evolução temporal */}
<Card>
<CardHeader><CardTitle>Evolução de Pedidos</CardTitle></CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data?.evolucao ?? []}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="mes" />
<YAxis />
<Tooltip />
<Legend />
<Line dataKey="totalPedidos" name="Pedidos" stroke="#1A292F" strokeWidth={2} />
<Line dataKey="somaPedidos" name="Valor Total" stroke="#F36F28" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Situação */}
<Card>
<CardHeader><CardTitle>Pedidos por Situação</CardTitle></CardHeader>
<CardContent className="h-[300px] flex justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Tooltip />
<Pie data={data?.situacoes ?? []} dataKey="total" nameKey="status" outerRadius={100} label>
{(data?.situacoes ?? []).map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Tipos de serviço */}
<Card>
<CardHeader><CardTitle>Distribuição por Tipo de Serviço</CardTitle></CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.tiposServico ?? []}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="servico_tipo" />
<YAxis />
<Tooltip />
<Bar dataKey="valor_total" fill="#F36F28" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Receitas */}
<Card>
<CardHeader><CardTitle>Receitas (Emolumentos e Taxas)</CardTitle></CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.receitas ?? []}>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="tipo" />
<YAxis />
<Tooltip />
<Bar dataKey="valor" fill="#8FB6C1" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Produtividade */}
<Card>
<CardHeader><CardTitle>Produtividade por Escrevente</CardTitle></CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data?.produtividade ?? []} layout="vertical">
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis type="number" />
<YAxis type="category" dataKey="escrevente" width={120} />
<Tooltip />
<Bar dataKey="totalPedidos" fill="#1A292F" radius={[0, 6, 6, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Qualidade */}
<Card>
<CardHeader><CardTitle>Qualidade de Dados</CardTitle></CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Quality label="Itens sem valor" value={data?.qualidade.itensSemValor ?? 0} />
<Quality label="Pedidos sem data" value={data?.qualidade.pedidosSemData ?? 0} />
<Quality label="Itens sem tipo" value={data?.qualidade.itensSemTipo ?? 0} />
</CardContent>
</Card>
<p className="text-xs text-center text-muted-foreground pt-4">
Fonte: VIEW VW_T_SERVICO_ANALYTICS fallback automático para mock.
</p>
</div>
)
}
// ========================
// Subcomponentes
// ========================
function Kpi({ icon: Icon, label, value }: { icon: any; label: string; value: any }) {
return (
<Card className="shadow-sm border-muted/40">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
<Icon className="h-5 w-5 text-muted-foreground" />
</CardHeader>
<CardContent><div className="text-2xl font-bold">{value ?? <Loader2 className="animate-spin" />}</div></CardContent>
</Card>
)
}
function Quality({ label, value }: { label: string; value: number }) {
const pct = Math.min(100, (value / 50) * 100)
return (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">{label}</p>
<Progress value={pct} />
<Badge variant={value > 0 ? 'destructive' : 'outline'}>{value}</Badge>
</div>
)
}
function formatCurrency(v?: number) {
if (!v) return 'R$ 0,00'
return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
}

View file

@ -0,0 +1,110 @@
'use client';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { ResetFormIfData } from '@/shared/actions/form/ResetFormIfData';
import LoadingButton from '@/shared/components/loadingButton/LoadingButton';
import { useTServicoPedidoFormHook } from '../../hooks/TServicoPedido/useTServicoPedidoFormHook';
import { TServicoPedidoFormInterface } from '../../interfaces/TServicoPedido/TServicoPedidoFormInterface';
/**
* Formulário de cadastro/edição de Natureza
* Baseado nos campos da tabela G_NATUREZA
*/
export default function TServicoPedidoForm({
isOpen,
data,
onClose,
onSave,
buttonIsLoading,
}: TServicoPedidoFormInterface) {
const form = useTServicoPedidoFormHook({});
// Atualiza o formulário quando recebe dados para edição
useEffect(() => {
ResetFormIfData(form, data);
}, [data, form]);
function onError(error: any) {
console.log('Erro no formulário:', error);
}
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose(null, false);
}}
>
<DialogContent className="w-full max-w-full p-6 sm:max-w-3xl md:max-w-2xl lg:max-w-2xl">
<DialogHeader>
<DialogTitle className="text-lg sm:text-xl">Formulário de Gramática</DialogTitle>
<DialogDescription className="text-muted-foreground text-sm">
Formulário de Gramática
</DialogDescription>
</DialogHeader>
{/* Formulário principal */}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSave, onError)} className="space-y-6">
{/* GRID MOBILE FIRST */}
<div className="grid w-full grid-cols-12 gap-4">
{/* Palavra */}
<div className="col-span-12 sm:col-span-6 md:col-span-12">
<FormField
control={form.control}
name="apresentante"
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Palavra</FormLabel>
<FormControl>
<Input
{...field}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Rodapé */}
<DialogFooter className="mt-6 flex flex-col justify-end gap-2 sm:flex-row">
<DialogClose asChild>
<Button variant="outline" type="button">
Cancelar
</Button>
</DialogClose>
<LoadingButton
text="Salvar"
textLoading="Salvando..."
type="submit"
loading={buttonIsLoading}
/>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,153 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useTServicoPedidoDeleteHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoDeleteHook';
import { useTServicoPedidoIndexHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoIndexHook';
import { useTServicoPedidoSaveHook } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoSaveHook';
import TServicoPedidoInterface from '@/packages/servicos/interfaces/TServicoPedido/TServicoPedidoInterface';
import ConfirmDialog from '@/shared/components/confirmDialog/ConfirmDialog';
import { useConfirmDialog } from '@/shared/components/confirmDialog/useConfirmDialog';
import Header from '@/shared/components/structure/Header';
import TServicoPedidoForm from './TServicoPedidoForm';
import TServicoPedidoTable from './TServicoPedidoTable';
export default function TServicoPedidoIndex() {
// Controle de estado do botão
const [buttonIsLoading, setButtonIsLoading] = useState(false);
// Hooks para leitura e salvamento
const { TServicoPedido, indexTServicoPedido } = useTServicoPedidoIndexHook();
const { saveTServicoPedido } = useTServicoPedidoSaveHook();
const { deleteTServicoPedido } = useTServicoPedidoDeleteHook();
// Estados
const [selectedData, setSelectedData] = useState<TServicoPedidoInterface | null>(null);
const [isFormOpen, setIsFormOpen] = useState(false);
// Estado para saber qual item será deletado
const [itemToDelete, setItemToDelete] = useState<TServicoPedidoInterface | null>(null);
/**
* Hook do modal de confirmação
*/
const { isOpen: isConfirmOpen, openDialog: openConfirmDialog, handleCancel } = useConfirmDialog();
/**
* Abre o formulário no modo de edição ou criação
*/
const handleOpenForm = useCallback((data: TServicoPedidoInterface | null) => {
setSelectedData(data);
setIsFormOpen(true);
}, []);
/**
* Fecha o formulário e limpa o andamento selecionado
*/
const handleCloseForm = useCallback(() => {
setSelectedData(null);
setIsFormOpen(false);
}, []);
/**
* Salva os dados do formulário
*/
const handleSave = useCallback(
async (formData: TServicoPedidoInterface) => {
// Coloca o botão em estado de loading
setButtonIsLoading(true);
// Aguarda salvar o registro
await saveTServicoPedido(formData);
// Remove o botão em estado de loading
setButtonIsLoading(false);
// Atualiza a lista de dados
indexTServicoPedido();
},
[saveTServicoPedido, indexTServicoPedido, handleCloseForm],
);
/**
* Quando o usuário clica em "remover" na tabela
*/
const handleConfirmDelete = useCallback(
(item: TServicoPedidoInterface) => {
// Define o item atual para remoção
setItemToDelete(item);
// Abre o modal de confirmação
openConfirmDialog();
},
[openConfirmDialog],
);
/**
* Executa a exclusão de fato quando o usuário confirma
*/
const handleDelete = useCallback(async () => {
// Protege contra null
if (!itemToDelete) return;
// Executa o Hook de remoção
await deleteTServicoPedido(itemToDelete);
// Atualiza a lista
await indexTServicoPedido();
// Limpa o item selecionado
setItemToDelete(null);
// Fecha o modal
handleCancel();
}, [itemToDelete, indexTServicoPedido, handleCancel]);
/**
* Busca inicial dos dados
*/
useEffect(() => {
// indexTServicoPedido();
}, []);
return (
<div>
{/* Cabeçalho */}
<Header
title={'Pedidos'}
description={'Pedidos de Autenticação/Reconhecimento'}
buttonText={'Novo pedido'}
buttonAction={() => {
handleOpenForm(null);
}}
/>
{/* Tabela de andamentos */}
<TServicoPedidoTable data={TServicoPedido} onEdit={handleOpenForm} onDelete={handleConfirmDelete} />
{/* Modal de confirmação */}
{isConfirmOpen && (
<ConfirmDialog
isOpen={isConfirmOpen}
title="Confirmar exclusão"
description="Atenção"
message={`Deseja realmente excluir o valor "${itemToDelete?.apresentante}"?`}
confirmText="Sim, excluir"
cancelText="Cancelar"
onConfirm={handleDelete}
onCancel={handleCancel}
/>
)}
{/* Formulário de criação/edição */}
{isFormOpen && (
<TServicoPedidoForm
isOpen={isFormOpen}
data={selectedData}
onClose={handleCloseForm}
onSave={handleSave}
buttonIsLoading={buttonIsLoading}
/>
)}
</div>
);
}

View file

@ -0,0 +1,23 @@
'use client';
import { DataTable } from '@/shared/components/dataTable/DataTable';
import TServicoPedidoTableInterface from '../../interfaces/TServicoPedido/TServicoPedidoTableInterface';
import TServicoPedidoColumns from './TServicoPedidoColumns';
/**
* Componente principal da tabela de Naturezas
*/
export default function TServicoPedidoTable({ data, onEdit, onDelete }: TServicoPedidoTableInterface) {
const columns = TServicoPedidoColumns(onEdit, onDelete);
return (
<div>
<DataTable
data={data}
columns={columns}
filterColumn="palavra"
filterPlaceholder="Buscar por descrição da natureza..."
/>
</div>
);
}

View file

@ -0,0 +1,16 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
async function executeTServicoItemPedidoDeleteData(data: TServicoItemPedidoInterface): Promise<ApiResponseInterface> {
const api = new API();
return api.send({
method: Methods.DELETE,
endpoint: `servicos/t_servico_itempedido/${data.servico_itempedido_id}`,
});
}
export const TServicoItemPedidoDeleteData = withClientErrorHandler(executeTServicoItemPedidoDeleteData);

View file

@ -0,0 +1,15 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
async function executeTServicoItemPedidoIndexData(): Promise<ApiResponseInterface> {
const api = new API();
return api.send({
method: Methods.GET,
endpoint: `servicos/t_servico_itempedido/`,
});
}
export const TServicoItemPedidoIndexData = withClientErrorHandler(executeTServicoItemPedidoIndexData);

View file

@ -0,0 +1,23 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
async function executeTServicoItemPedidoSaveData(data: TServicoItemPedidoInterface): Promise<ApiResponseInterface> {
// Verifica se existe ID para decidir se é atualização (PUT) ou criação (POST)
const isUpdate = Boolean(data.servico_itempedido_id);
// Instancia o cliente da API
const api = new API();
// Executa a requisição para a API com o método apropriado e envia os dados no corpo
return api.send({
method: isUpdate ? Methods.PUT : Methods.POST, // PUT se atualizar, POST se criar
endpoint: `servicos/t_servico_itempedido/${data.servico_itempedido_id || ''}`, // endpoint dinâmico
body: data, // payload enviado para a API
});
}
export const TServicoItemPedidoSaveData = withClientErrorHandler(executeTServicoItemPedidoSaveData);

View file

@ -0,0 +1,17 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
async function executeTServicoPedidoDeleteData(data: TServicoPedidoInterface): Promise<ApiResponseInterface> {
const api = new API();
return api.send({
method: Methods.DELETE,
endpoint: `servico/t_servico_pedido/${data.servico_pedido_id}`,
});
}
export const TServicoPedidoDeleteData = withClientErrorHandler(executeTServicoPedidoDeleteData);

View file

@ -0,0 +1,15 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
async function executeTServicoPedidoIndexData(): Promise<ApiResponseInterface> {
const api = new API();
return api.send({
method: Methods.GET,
endpoint: `servico/t_servico_pedido/`,
});
}
export const TServicoPedidoIndexData = withClientErrorHandler(executeTServicoPedidoIndexData);

View file

@ -0,0 +1,23 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import API from '@/shared/services/api/Api';
import { Methods } from '@/shared/services/api/enums/ApiMethodEnum';
import ApiResponseInterface from '@/shared/services/api/interfaces/ApiResponseInterface';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
async function executeTServicoPedidoSaveData(data: TServicoPedidoInterface): Promise<ApiResponseInterface> {
// Verifica se existe ID para decidir se é atualização (PUT) ou criação (POST)
const isUpdate = Boolean(data.servico_pedido_id);
// Instancia o cliente da API
const api = new API();
// Executa a requisição para a API com o método apropriado e envia os dados no corpo
return api.send({
method: isUpdate ? Methods.PUT : Methods.POST, // PUT se atualizar, POST se criar
endpoint: `servico/t_servico_pedido/${data.servico_pedido_id || ''}`, // endpoint dinâmico
body: data, // payload enviado para a API
});
}
export const TServicoPedidoSaveData = withClientErrorHandler(executeTServicoPedidoSaveData);

View file

@ -0,0 +1,21 @@
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
import { TServicoItemPedidoDeleteService } from '../../services/TServicoItemPedido/TServicoItemPedidoDeleteService';
export const useTServicoItemPedidoDeleteHook = () => {
const { setResponse } = useResponse();
const [TServicoItemPedido, setTServicoItemPedido] = useState<TServicoItemPedidoInterface>();
const deleteTServicoItemPedido = async (data: TServicoItemPedidoInterface) => {
const response = await TServicoItemPedidoDeleteService(data);
setTServicoItemPedido(data);
setResponse(response);
};
return { TServicoItemPedido, deleteTServicoItemPedido };
};

View file

@ -0,0 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { TServicoItemPedidoFormValues, TServicoItemPedidoSchema } from '../../schemas/TServicoItemPedido/TServicoItemPedidoSchema';
export function useTServicoItemPedidoFormHook(defaults?: Partial<TServicoItemPedidoFormValues>) {
return useForm<TServicoItemPedidoFormValues>({
resolver: zodResolver(TServicoItemPedidoSchema),
defaultValues: {
servico_itempedido_id: 0,
...defaults,
},
});
}

View file

@ -0,0 +1,28 @@
'use client';
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
import { TServicoItemPedidoIndexService } from '../../services/TServicoItemPedido/TServicoItemPedidoIndexService';
export const useTServicoItemPedidoIndexHook = () => {
const { setResponse } = useResponse();
const [TServicoItemPedido, setTServicoItemPedido] = useState<TServicoItemPedidoInterface[]>([]);
const indexTServicoItemPedido = async () => {
const response = await TServicoItemPedidoIndexService();
// Armazena os dados consultados
setTServicoItemPedido(response.data);
// Define a resposta (toast, modal, feedback, etc.)
setResponse(response);
};
return {
TServicoItemPedido,
indexTServicoItemPedido,
};
};

View file

@ -0,0 +1,35 @@
'use client';
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
import { TServicoItemPedidoSaveService } from '../../services/TServicoItemPedido/TServicoItemPedidoSaveService';
export const useTServicoItemPedidoSaveHook = () => {
const { setResponse } = useResponse();
const [TServicoItemPedido, setTServicoItemPedido] = useState<TServicoItemPedidoInterface | null>(null);
// controla se o formulário está aberto ou fechado
const [isOpen, setIsOpen] = useState(false);
const saveTServicoItemPedido = async (data: TServicoItemPedidoInterface) => {
const response = await TServicoItemPedidoSaveService(data);
// Armazena os dados da resposta
setTServicoItemPedido(response.data);
// Define os dados da resposta (toast, modal, etc.)
setResponse(response);
// Fecha o formulário automaticamente após salvar
setIsOpen(false);
// Retorna os valores de forma imediata
return response.data;
};
return { TServicoItemPedido, saveTServicoItemPedido, isOpen, setIsOpen };
};

View file

@ -0,0 +1,17 @@
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
import { TServicoPedidoDeleteService } from '../../services/TServicoPedido/TServicoPedidoDeleteService';
export const useTServicoPedidoDeleteHook = () => {
const { setResponse } = useResponse();
const [TServicoPedido, setTServicoPedido] = useState<TServicoPedidoInterface>();
const deleteTServicoPedido = async (data: TServicoPedidoInterface) => {
const response = await TServicoPedidoDeleteService(data);
setTServicoPedido(data);
setResponse(response);
};
return { TServicoPedido, deleteTServicoPedido };
};

View file

@ -0,0 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { TServicoPedidoFormValues, TServicoPedidoSchema } from '../../schemas/TServicoPedido/TServicoPedidoSchema';
export function useTServicoPedidoFormHook(defaults?: Partial<TServicoPedidoFormValues>) {
return useForm<TServicoPedidoFormValues>({
resolver: zodResolver(TServicoPedidoSchema),
defaultValues: {
servico_pedido_id: 0,
...defaults,
},
});
}

View file

@ -0,0 +1,27 @@
'use client';
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
import { TServicoPedidoIndexService } from '../../services/TServicoPedido/TServicoPedidoIndexService';
export const useTServicoPedidoIndexHook = () => {
const { setResponse } = useResponse();
const [TServicoPedido, setTServicoPedido] = useState<TServicoPedidoInterface[]>([]);
const indexTServicoPedido = async () => {
const response = await TServicoPedidoIndexService();
// Armazena os dados consultados
setTServicoPedido(response.data);
// Define a resposta (toast, modal, feedback, etc.)
setResponse(response);
};
return {
TServicoPedido,
indexTServicoPedido,
};
};

View file

@ -0,0 +1,35 @@
'use client';
import { useState } from 'react';
import { useResponse } from '@/shared/components/response/ResponseContext';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
import { TServicoPedidoSaveService } from '../../services/TServicoPedido/TServicoPedidoSaveService';
export const useTServicoPedidoSaveHook = () => {
const { setResponse } = useResponse();
const [TServicoPedido, setTServicoPedido] = useState<TServicoPedidoInterface | null>(null);
// controla se o formulário está aberto ou fechado
const [isOpen, setIsOpen] = useState(false);
const saveTServicoPedido = async (data: TServicoPedidoInterface) => {
const response = await TServicoPedidoSaveService(data);
// Armazena os dados da resposta
setTServicoPedido(response.data);
// Define os dados da resposta (toast, modal, etc.)
setResponse(response);
// Fecha o formulário automaticamente após salvar
setIsOpen(false);
// Retorna os valores de forma imediata
return response.data;
};
return { TServicoPedido, saveTServicoPedido, isOpen, setIsOpen };
};

View file

@ -0,0 +1,9 @@
import { TServicoItemPedidoFormValues } from '../../schemas/TServicoItemPedido/TServicoItemPedidoSchema';
export interface TServicoItemPedidoFormInterface {
isOpen: boolean;
data: TServicoItemPedidoFormValues | null;
onClose: (item: null, isFormStatus: boolean) => void;
onSave: (data: TServicoItemPedidoFormValues) => void;
buttonIsLoading: boolean;
}

View file

@ -0,0 +1,66 @@
export default interface TServicoItemPedidoInterface {
servico_itempedido_id?: number
servico_pedido_id?: number
servico_tipo_id?: number
valor?: number
qtd?: number
pessoa_id?: number
impressao_etiqueta?: string
situacao?: string
etiqueta_numero?: number
pessoa_auxiliar_id?: number
pessoa_sp_abono_rep?: string
tipo_item?: string
imprimir?: string
observacao?: string
impressao_direta?: string
selo_livro_id?: number
emolumento?: number
fundesp?: number
taxa_judiciaria?: number
desconto?: number
desc_complementar?: string
valor_manual?: string
valor_documento?: number
outra_taxa1?: number
emolumento_item_id?: number
certidao_impressa?: string
certidao_ato_id?: number
emolumento_id?: number
certidao_previsao?: string | Date
certidao_ato_antigo?: string
certidao_data_emissao?: string | Date
certidao_texto?: string | null // BLOB -> texto longo ou base64
ato_antigo_tipo?: string
valor_iss?: number
id_ato_isentado?: number
motivo_isencao?: string
pessoas_etiquetas?: number
abonador?: string
servico_cartao?: string
valor_informacoes_centrais?: number
situacao_diferido?: string
sigla_numero?: string
motivo_diferido?: string
nome_juridico?: string
etiqueta_apenas_frente?: string
indexacao_id?: number
certidao_data_lavratura?: string | Date
nfse_id?: number
qtd_pagina_certidao?: number
placa?: string
dut?: string
etiqueta_unica?: string
fundo_abonador?: string
instrumento_publico?: string
data_lavratura_abono?: string | Date
valor_base_calculo?: number
valor_avaliacao?: number
ato_abonado?: number
transferencia_veiculo?: string
usar_a4?: string
cpf_abono_rep?: string
vrcext?: number
valor_fundo_selo?: number
averbacao?: string
}

View file

@ -0,0 +1,7 @@
import TServicoItemPedidoInterface from "./TServicoItemPedidoIntefarce";
export default interface TServicoItemPedidoTableInterface {
data?: TServicoItemPedidoInterface[];
onEdit: (item: TServicoItemPedidoInterface, isEditingFormStatus: boolean) => void;
onDelete: (item: TServicoItemPedidoInterface, isEditingFormStatus: boolean) => void;
}

View file

@ -0,0 +1,9 @@
import { TServicoPedidoFormValues } from '../../schemas/TServicoPedido/TServicoPedidoSchema';
export interface TServicoPedidoFormInterface {
isOpen: boolean;
data: TServicoPedidoFormValues | null;
onClose: (item: null, isFormStatus: boolean) => void;
onSave: (data: TServicoPedidoFormValues) => void;
buttonIsLoading: boolean;
}

View file

@ -0,0 +1,15 @@
export default interface TServicoPedidoInterface {
servico_pedido_id?: number;
valor_pedido?: number;
valor_pago?: number;
usuario_id?: number;
data_pedido?: string;
mensalista_livrocaixa_id?: number;
observacao?: string;
escrevente_id?: number;
situacao?: string;
estornado?: string;
apresentante?: string;
nfse_id?: number;
cpfcnpj_apresentante?: string;
}

View file

@ -0,0 +1,7 @@
import TServicoPedidoInterface from './TServicoPedidoInterface';
export default interface TServicoPedidoTableInterface {
data?: TServicoPedidoInterface[];
onEdit: (item: TServicoPedidoInterface, isEditingFormStatus: boolean) => void;
onDelete: (item: TServicoPedidoInterface, isEditingFormStatus: boolean) => void;
}

View file

@ -0,0 +1,70 @@
import z from "zod"
export const TServicoItemPedidoSchema = z.object({
servico_itempedido_id: z.number().optional(),
servico_pedido_id: z.number().optional(),
servico_tipo_id: z.number().optional(),
valor: z.number().optional(),
qtd: z.number().optional(),
pessoa_id: z.number().optional(),
impressao_etiqueta: z.string().optional(),
situacao: z.string().optional(),
etiqueta_numero: z.number().optional(),
pessoa_auxiliar_id: z.number().optional(),
pessoa_sp_abono_rep: z.string().optional(),
tipo_item: z.string().optional(),
imprimir: z.string().optional(),
observacao: z.string().optional(),
impressao_direta: z.string().optional(),
selo_livro_id: z.number().optional(),
emolumento: z.number().optional(),
fundesp: z.number().optional(),
taxa_judiciaria: z.number().optional(),
desconto: z.number().optional(),
desc_complementar: z.string().optional(),
valor_manual: z.string().optional(),
valor_documento: z.number().optional(),
outra_taxa1: z.number().optional(),
emolumento_item_id: z.number().optional(),
certidao_impressa: z.string().optional(),
certidao_ato_id: z.number().optional(),
emolumento_id: z.number().optional(),
certidao_previsao: z.union([z.string(), z.date()]).optional(),
certidao_ato_antigo: z.string().optional(),
certidao_data_emissao: z.union([z.string(), z.date()]).optional(),
certidao_texto: z.string().nullable().optional(),
ato_antigo_tipo: z.string().optional(),
valor_iss: z.number().optional(),
id_ato_isentado: z.number().optional(),
motivo_isencao: z.string().optional(),
pessoas_etiquetas: z.number().optional(),
abonador: z.string().optional(),
servico_cartao: z.string().optional(),
valor_informacoes_centrais: z.number().optional(),
situacao_diferido: z.string().optional(),
sigla_numero: z.string().optional(),
motivo_diferido: z.string().optional(),
nome_juridico: z.string().optional(),
etiqueta_apenas_frente: z.string().optional(),
indexacao_id: z.number().optional(),
certidao_data_lavratura: z.union([z.string(), z.date()]).optional(),
nfse_id: z.number().optional(),
qtd_pagina_certidao: z.number().optional(),
placa: z.string().optional(),
dut: z.string().optional(),
etiqueta_unica: z.string().optional(),
fundo_abonador: z.string().optional(),
instrumento_publico: z.string().optional(),
data_lavratura_abono: z.union([z.string(), z.date()]).optional(),
valor_base_calculo: z.number().optional(),
valor_avaliacao: z.number().optional(),
ato_abonado: z.number().optional(),
transferencia_veiculo: z.string().optional(),
usar_a4: z.string().optional(),
cpf_abono_rep: z.string().optional(),
vrcext: z.number().optional(),
valor_fundo_selo: z.number().optional(),
averbacao: z.string().optional(),
})
export type TServicoItemPedidoFormValues = z.infer<typeof TServicoItemPedidoSchema>

View file

@ -0,0 +1,19 @@
import z from "zod";
export const TServicoPedidoSchema = z.object({
servico_pedido_id: z.number().optional,
valor_pedido: z.number().optional,
valor_pago: z.number().optional,
usuario_id: z.number().optional,
data_pedido: z.string().optional,
mensalista_livrocaixa_id: z.number().optional,
observacao: z.string().optional,
escrevente_id: z.number().optional,
situacao: z.string().optional,
estornado: z.string().optional,
apresentante: z.string().optional,
nfse_id: z.number().optional,
cpfcnpj_apresentante: z.string().optional,
});
export type TServicoPedidoFormValues = z.infer<typeof TServicoPedidoSchema>;

View file

@ -0,0 +1,14 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { TServicoItemPedidoDeleteData } from '../../data/TServicoItemPedido/TServicoItemPedidoDeleteData';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
async function executeTServicoItemPedidoDeleteService(data: TServicoItemPedidoInterface) {
const response = await TServicoItemPedidoDeleteData(data);
return response;
}
export const TServicoItemPedidoDeleteService = withClientErrorHandler(executeTServicoItemPedidoDeleteService);

View file

@ -0,0 +1,10 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { TServicoItemPedidoIndexData } from '../../data/TServicoItemPedido/TServicoItemPedidoIndexData';
export default async function executeTServicoItemPedidoIndexService() {
const response = await TServicoItemPedidoIndexData();
return response;
}
export const TServicoItemPedidoIndexService = withClientErrorHandler(executeTServicoItemPedidoIndexService);

View file

@ -0,0 +1,12 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { TServicoItemPedidoSaveData } from '../../data/TServicoItemPedido/TServicoItemPedidoSaveData';
import TServicoItemPedidoInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
async function executeTServicoItemPedidoSaveService(data: TServicoItemPedidoInterface) {
const response = await TServicoItemPedidoSaveData(data);
return response;
}
export const TServicoItemPedidoSaveService = withClientErrorHandler(executeTServicoItemPedidoSaveService);

View file

@ -0,0 +1,13 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { TServicoPedidoDeleteData } from '../../data/TServicoPedido/TServicoPedidoDeleteData';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
async function executeTServicoPedidoDeleteService(data: TServicoPedidoInterface) {
const response = await TServicoPedidoDeleteData(data);
return response;
}
export const TServicoPedidoDeleteService = withClientErrorHandler(executeTServicoPedidoDeleteService);

View file

@ -0,0 +1,10 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { TServicoPedidoIndexData } from '../../data/TServicoPedido/TServicoPedidoIndexData';
export default async function executeTServicoPedidoIndexService() {
const response = await TServicoPedidoIndexData();
return response;
}
export const TServicoPedidoIndexService = withClientErrorHandler(executeTServicoPedidoIndexService);

View file

@ -0,0 +1,11 @@
import { withClientErrorHandler } from '@/shared/actions/withClientErrorHandler/withClientErrorHandler';
import { TServicoPedidoSaveData } from '../../data/TServicoPedido/TServicoPedidoSaveData';
import TServicoPedidoInterface from '../../interfaces/TServicoPedido/TServicoPedidoInterface';
async function executeTServicoPedidoSaveService(data: TServicoPedidoInterface) {
const response = await TServicoPedidoSaveData(data);
return response;
}
export const TServicoPedidoSaveService = withClientErrorHandler(executeTServicoPedidoSaveService);