[MVPTN-37] feat(CRUD): Implementação inicial do CRUD de serviços de balcão
This commit is contained in:
parent
8e035546e9
commit
e39422776e
56 changed files with 3606 additions and 99 deletions
482
package-lock.json
generated
482
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export default function TCensecPage() {
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import TImovelDashboard from "@/packages/administrativo/components/TImovel/TImovelDashboard";
|
||||
|
||||
export default function TImovelDashboardPage() {
|
||||
return (
|
||||
<TImovelDashboard />
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import TPessoaDashboard from "@/packages/administrativo/components/TPessoa/TPessoaDashboard";
|
||||
|
||||
export default function TPessoaDashboardPage() {
|
||||
return (
|
||||
<TPessoaDashboard />
|
||||
);
|
||||
}
|
||||
7
src/app/(protected)/servicos/balcao/page.tsx
Normal file
7
src/app/(protected)/servicos/balcao/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import TServicoPedidoIndex from "@/packages/servicos/components/TServicoPedido/TServicoPedidoIndex";
|
||||
|
||||
export default function TServicoPedidoPage() {
|
||||
return (
|
||||
<TServicoPedidoIndex />
|
||||
)
|
||||
}
|
||||
90
src/app/(protected)/servicos/balcao/pedido/page.tsx
Normal file
90
src/app/(protected)/servicos/balcao/pedido/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(protected)/servicos/dashboard/page.tsx
Normal file
7
src/app/(protected)/servicos/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import TServicoPedidoDashboard from "@/packages/servicos/components/TServicoPedido/TServicoPedidoDashboard";
|
||||
|
||||
export default function TServicoPedidoPage() {
|
||||
return (
|
||||
<TServicoPedidoDashboard />
|
||||
)
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal 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 }
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal 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 }
|
||||
58
src/components/ui/scroll-area.tsx
Normal file
58
src/components/ui/scroll-area.tsx
Normal 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 }
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 Nº {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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
@ -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' })
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
Loading…
Add table
Reference in a new issue