Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71f9c9ab86 | |||
| 76cbd80eef |
Generated
+17
-1
@@ -2911,6 +2911,7 @@
|
|||||||
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -2928,6 +2929,7 @@
|
|||||||
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
|
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -2939,6 +2941,7 @@
|
|||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
}
|
}
|
||||||
@@ -2989,6 +2992,7 @@
|
|||||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.38.0",
|
"@typescript-eslint/scope-manager": "8.38.0",
|
||||||
"@typescript-eslint/types": "8.38.0",
|
"@typescript-eslint/types": "8.38.0",
|
||||||
@@ -3221,6 +3225,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3442,6 +3447,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001726",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.173",
|
"electron-to-chromium": "^1.5.173",
|
||||||
@@ -3800,6 +3806,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
@@ -3904,7 +3911,8 @@
|
|||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
@@ -4047,6 +4055,7 @@
|
|||||||
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5742,6 +5751,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -5934,6 +5944,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -5960,6 +5971,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -5973,6 +5985,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.61.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.61.1.tgz",
|
||||||
"integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==",
|
"integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -6526,6 +6539,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
||||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -6650,6 +6664,7 @@
|
|||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6829,6 +6844,7 @@
|
|||||||
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|||||||
+5
-1
@@ -9,6 +9,7 @@ import NotFound from "./pages/NotFound";
|
|||||||
import Redirect from "./pages/Redirect";
|
import Redirect from "./pages/Redirect";
|
||||||
import { GlobalFunctions } from "./GlobalFunctions";
|
import { GlobalFunctions } from "./GlobalFunctions";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import IntelligenceIAApp from "./modules/intelligence-ia/App";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -25,8 +26,11 @@ const App = () => (
|
|||||||
element={<Redirect />}
|
element={<Redirect />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Módulo Intelligence IA como subpath*/}
|
||||||
|
<Route path="/intelligence-ia/*" element={<IntelligenceIAApp />} />
|
||||||
|
|
||||||
<Route path="/*" element={<Index />} />
|
<Route path="/*" element={<Index />} />
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
<Route path="/404" element={<NotFound />} />
|
<Route path="/404" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
+72
-1
@@ -31,6 +31,12 @@
|
|||||||
--destructive: 0 84% 60%;
|
--destructive: 0 84% 60%;
|
||||||
--destructive-foreground: 0 0% 100%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
|
||||||
|
--success: 142 71% 45%;
|
||||||
|
--success-foreground: 0 0% 100%;
|
||||||
|
|
||||||
|
--warning: 38 92% 50%;
|
||||||
|
--warning-foreground: 222 47% 11%;
|
||||||
|
|
||||||
--border: 214 32% 91%;
|
--border: 214 32% 91%;
|
||||||
--input: 214 32% 91%;
|
--input: 214 32% 91%;
|
||||||
--ring: 190 100% 45%;
|
--ring: 190 100% 45%;
|
||||||
@@ -84,6 +90,12 @@
|
|||||||
--destructive: 0 84% 60%;
|
--destructive: 0 84% 60%;
|
||||||
--destructive-foreground: 200 100% 95%;
|
--destructive-foreground: 200 100% 95%;
|
||||||
|
|
||||||
|
--success: 142 71% 45%;
|
||||||
|
--success-foreground: 222 47% 5%;
|
||||||
|
|
||||||
|
--warning: 38 92% 50%;
|
||||||
|
--warning-foreground: 222 47% 5%;
|
||||||
|
|
||||||
--border: 222 30% 18%;
|
--border: 222 30% 18%;
|
||||||
--input: 222 30% 18%;
|
--input: 222 30% 18%;
|
||||||
--ring: 190 100% 50%;
|
--ring: 190 100% 50%;
|
||||||
@@ -137,7 +149,7 @@
|
|||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.glass-effect {
|
.glass-effect {
|
||||||
@apply bg-card/40 backdrop-blur-xl border border-border/50;
|
@apply bg-card/80 backdrop-blur-xl border border-border/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cyber-glow {
|
.cyber-glow {
|
||||||
@@ -152,4 +164,63 @@
|
|||||||
.gradient-text {
|
.gradient-text {
|
||||||
@apply bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent;
|
@apply bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
@apply bg-card rounded-xl p-5 border border-border shadow-sm transition-all duration-300 hover:shadow-md hover:border-primary/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
@apply flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item svg,
|
||||||
|
.nav-item .w-5 {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-inactive {
|
||||||
|
@apply text-muted-foreground hover:text-foreground hover:bg-sidebar-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-active {
|
||||||
|
@apply text-primary bg-sidebar-accent font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
@apply w-2 h-2 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-connected {
|
||||||
|
@apply bg-success;
|
||||||
|
box-shadow: 0 0 8px hsl(var(--success) / 0.6);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
@apply bg-destructive;
|
||||||
|
box-shadow: 0 0 8px hsl(var(--destructive) / 0.6);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-warning {
|
||||||
|
@apply bg-warning;
|
||||||
|
box-shadow: 0 0 8px hsl(var(--warning) / 0.6);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import { MainLayout } from "@/modules/intelligence-ia/components/layout/MainLayout";
|
||||||
|
import Index from "./pages/Index";
|
||||||
|
import Integrations from "./pages/Integrations";
|
||||||
|
import Financas from "./pages/Financas";
|
||||||
|
import MeuPerfil from "./pages/MeuPerfil";
|
||||||
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
|
// App do módulo Intelligence IA - funciona como subpath
|
||||||
|
// Não precisa de BrowserRouter, QueryClientProvider, TooltipProvider, Toaster ou Sonner
|
||||||
|
// pois já são fornecidos pelo App principal
|
||||||
|
const IntelligenceIAApp = () => {
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Index />} />
|
||||||
|
<Route path="integracoes" element={<Integrations />} />
|
||||||
|
<Route path="financas" element={<Financas />} />
|
||||||
|
<Route path="meu-perfil" element={<MeuPerfil />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IntelligenceIAApp;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 831 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
|
||||||
|
className?: string;
|
||||||
|
activeClassName?: string;
|
||||||
|
pendingClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
|
||||||
|
({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RouterNavLink
|
||||||
|
ref={ref}
|
||||||
|
to={to}
|
||||||
|
className={({ isActive, isPending }) =>
|
||||||
|
cn(className, isActive && activeClassName, isPending && pendingClassName)
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
NavLink.displayName = "NavLink";
|
||||||
|
|
||||||
|
export { NavLink };
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface MetricCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
subtitle?: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
status?: "success" | "warning" | "error";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricCard({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
subtitle,
|
||||||
|
icon,
|
||||||
|
status,
|
||||||
|
className,
|
||||||
|
}: MetricCardProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn("metric-card animate-fade-in", className)}>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="p-2.5 rounded-lg bg-primary/10 text-primary">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
{status && (
|
||||||
|
<span
|
||||||
|
className={cn("status-dot", {
|
||||||
|
"status-connected": status === "success",
|
||||||
|
"status-warning": status === "warning",
|
||||||
|
"status-error": status === "error",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-sm mb-1">{title}</p>
|
||||||
|
<p className="text-2xl font-semibold text-foreground">{value}</p>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-muted-foreground text-xs mt-1">{subtitle}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface StatusBadgeProps {
|
||||||
|
status: "connected" | "error" | "disconnected" | "pending";
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
connected: {
|
||||||
|
label: "Conectado",
|
||||||
|
className: "bg-success/10 text-success border-success/20",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
label: "Erro",
|
||||||
|
className: "bg-destructive/10 text-destructive border-destructive/20",
|
||||||
|
},
|
||||||
|
disconnected: {
|
||||||
|
label: "Desconectado",
|
||||||
|
className: "bg-muted text-muted-foreground border-border",
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
label: "Pendente",
|
||||||
|
className: "bg-warning/10 text-warning border-warning/20",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StatusBadge({ status, label }: StatusBadgeProps) {
|
||||||
|
const config = statusConfig[status];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border",
|
||||||
|
config.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn("w-1.5 h-1.5 rounded-full", {
|
||||||
|
"bg-success": status === "connected",
|
||||||
|
"bg-destructive": status === "error",
|
||||||
|
"bg-muted-foreground": status === "disconnected",
|
||||||
|
"bg-warning": status === "pending",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{label || config.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Eye, EyeOff, Loader2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import asanaLogo from "@/modules/intelligence-ia/assets/asana-logo.png";
|
||||||
|
|
||||||
|
interface Workspace {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsanaCardProps {
|
||||||
|
apiKey?: string;
|
||||||
|
workspaces?: Workspace[];
|
||||||
|
users?: User[];
|
||||||
|
selectedWorkspaceId?: string;
|
||||||
|
selectedUserId?: string;
|
||||||
|
loadingWorkspaces?: boolean;
|
||||||
|
loadingUsers?: boolean;
|
||||||
|
loadingIntegration?: boolean;
|
||||||
|
hasIntegration?: boolean;
|
||||||
|
saving?: boolean;
|
||||||
|
onSave?: (apiKey: string, workspaceId: string, userId: string) => void | Promise<void>;
|
||||||
|
onConfirmApiKey?: (apiKey: string) => void | Promise<void>;
|
||||||
|
onWorkspaceChange?: (workspaceId: string) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsanaCard({
|
||||||
|
apiKey = "",
|
||||||
|
workspaces = [],
|
||||||
|
users = [],
|
||||||
|
selectedWorkspaceId = "",
|
||||||
|
selectedUserId = "",
|
||||||
|
loadingWorkspaces = false,
|
||||||
|
loadingUsers = false,
|
||||||
|
loadingIntegration = false,
|
||||||
|
hasIntegration = false,
|
||||||
|
saving = false,
|
||||||
|
onSave,
|
||||||
|
onConfirmApiKey,
|
||||||
|
onWorkspaceChange,
|
||||||
|
}: AsanaCardProps) {
|
||||||
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
const [key, setKey] = useState(apiKey);
|
||||||
|
const [selectedWorkspace, setSelectedWorkspace] = useState(selectedWorkspaceId);
|
||||||
|
const [selectedUser, setSelectedUser] = useState(selectedUserId);
|
||||||
|
const [isApiKeyConfirmed, setIsApiKeyConfirmed] = useState(!!apiKey);
|
||||||
|
|
||||||
|
// Atualiza os valores quando as props mudam (carregamento inicial)
|
||||||
|
useEffect(() => {
|
||||||
|
if (apiKey) {
|
||||||
|
setKey(apiKey);
|
||||||
|
setIsApiKeyConfirmed(true);
|
||||||
|
}
|
||||||
|
}, [apiKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedWorkspaceId) {
|
||||||
|
setSelectedWorkspace(selectedWorkspaceId);
|
||||||
|
}
|
||||||
|
}, [selectedWorkspaceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedUserId) {
|
||||||
|
setSelectedUser(selectedUserId);
|
||||||
|
}
|
||||||
|
}, [selectedUserId]);
|
||||||
|
|
||||||
|
const handleKeyChange = (value: string) => {
|
||||||
|
setKey(value);
|
||||||
|
setSelectedWorkspace("");
|
||||||
|
setSelectedUser("");
|
||||||
|
setIsApiKeyConfirmed(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmApiKey = async () => {
|
||||||
|
if (key.length >= 5) {
|
||||||
|
setIsApiKeyConfirmed(true);
|
||||||
|
// Chama a função assíncrona para buscar workspaces
|
||||||
|
await onConfirmApiKey?.(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkspaceChange = async (value: string) => {
|
||||||
|
setSelectedWorkspace(value);
|
||||||
|
setSelectedUser("");
|
||||||
|
// Chama a função assíncrona para buscar usuários
|
||||||
|
await onWorkspaceChange?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
await onSave?.(key, selectedWorkspace, selectedUser);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="metric-card space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
|
||||||
|
<img src={asanaLogo} alt="Asana" className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-foreground font-medium">Asana</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">Gerencie tarefas e projetos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
API Key / Token
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
{loadingIntegration ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
type={showKey ? "text" : "password"}
|
||||||
|
value={key}
|
||||||
|
onChange={(e) => handleKeyChange(e.target.value)}
|
||||||
|
placeholder="Insira sua chave de API"
|
||||||
|
className="pr-10 bg-card border-border"
|
||||||
|
disabled={loadingWorkspaces}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowKey(!showKey)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
disabled={loadingWorkspaces}
|
||||||
|
>
|
||||||
|
{showKey ? (
|
||||||
|
<EyeOff className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleConfirmApiKey}
|
||||||
|
disabled={key.length < 5 || isApiKeyConfirmed || loadingWorkspaces}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{loadingWorkspaces ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Carregando...
|
||||||
|
</>
|
||||||
|
) : isApiKeyConfirmed ? (
|
||||||
|
"Confirmado"
|
||||||
|
) : (
|
||||||
|
"Confirmar"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Workspace
|
||||||
|
</label>
|
||||||
|
{loadingIntegration ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">Carregando integração...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
<Select
|
||||||
|
value={selectedWorkspace}
|
||||||
|
onValueChange={handleWorkspaceChange}
|
||||||
|
disabled={loadingWorkspaces || loadingIntegration}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-card border-border">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
loadingWorkspaces
|
||||||
|
? "Carregando workspaces..."
|
||||||
|
: workspaces.length === 0
|
||||||
|
? "Insira o token e clique em Confirmar"
|
||||||
|
: "Selecione um workspace"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-card border-border">
|
||||||
|
{workspaces.map((ws) => (
|
||||||
|
<SelectItem key={ws.id} value={ws.id}>
|
||||||
|
{ws.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{loadingWorkspaces && (
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Usuário
|
||||||
|
</label>
|
||||||
|
{loadingUsers ? (
|
||||||
|
<div className="relative">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">Carregando usuários...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative">
|
||||||
|
<Select
|
||||||
|
value={selectedUser}
|
||||||
|
onValueChange={setSelectedUser}
|
||||||
|
disabled={loadingUsers || !selectedWorkspace}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-card border-border">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
!selectedWorkspace
|
||||||
|
? "Selecione um workspace primeiro"
|
||||||
|
: users.length === 0
|
||||||
|
? "Nenhum usuário encontrado"
|
||||||
|
: "Selecione um usuário"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="bg-card border-border">
|
||||||
|
{users.map((user) => (
|
||||||
|
<SelectItem key={user.id} value={user.id}>
|
||||||
|
{user.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!selectedWorkspace || !selectedUser || saving}
|
||||||
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
{hasIntegration ? "Salvando..." : "Criando..."}
|
||||||
|
</>
|
||||||
|
) : hasIntegration ? (
|
||||||
|
"Salvar"
|
||||||
|
) : (
|
||||||
|
"Criar Integração"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import googleCalendarLogo from "@/modules/intelligence-ia/assets/google-calendar-logo.png";
|
||||||
|
|
||||||
|
export function GoogleCalendarCard() {
|
||||||
|
return (
|
||||||
|
<div className="metric-card space-y-4 opacity-60">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
|
||||||
|
<img src={googleCalendarLogo} alt="Google Calendar" className="w-6 h-6 grayscale" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-foreground font-medium">Google Calendar</h3>
|
||||||
|
<span className="text-xs bg-muted text-muted-foreground px-2 py-0.5 rounded-full">
|
||||||
|
Em breve
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-sm">Sincronize reuniões e eventos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 pointer-events-none">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Client ID
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
placeholder="Insira o Client ID"
|
||||||
|
className="bg-card border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Client Secret
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
disabled
|
||||||
|
placeholder="Insira o Client Secret"
|
||||||
|
className="bg-card border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Nome da Agenda
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
placeholder="Ex: Reuniões de Trabalho"
|
||||||
|
className="bg-card border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled
|
||||||
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Salvar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { FileSpreadsheet } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
export function GoogleSheetsCard() {
|
||||||
|
return (
|
||||||
|
<div className="metric-card space-y-4 opacity-60">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
|
||||||
|
<FileSpreadsheet className="w-6 h-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-foreground font-medium">Google Sheets</h3>
|
||||||
|
<span className="text-xs bg-muted text-muted-foreground px-2 py-0.5 rounded-full">
|
||||||
|
Em breve
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-sm">Gerencie planilhas e dados</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 pointer-events-none">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Client ID
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
placeholder="Insira o Client ID"
|
||||||
|
className="bg-card border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Client Secret
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
disabled
|
||||||
|
placeholder="Insira o Client Secret"
|
||||||
|
className="bg-card border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||||
|
Link da Planilha
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
placeholder="Cole o link da planilha aqui"
|
||||||
|
className="bg-card border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
disabled
|
||||||
|
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
Salvar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Plug,
|
||||||
|
Bot,
|
||||||
|
ChevronLeft,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
|
DollarSign,
|
||||||
|
UserCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ title: "Home", path: "", icon: LayoutDashboard },
|
||||||
|
{ title: "Finanças", path: "financas", icon: DollarSign },
|
||||||
|
{ title: "Integrações", path: "integracoes", icon: Plug },
|
||||||
|
{ title: "Meu Perfil", path: "meu-perfil", icon: UserCircle },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AppSidebar() {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
|
const SidebarContent = () => (
|
||||||
|
<>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-3 px-4 py-6 border-b border-sidebar-border">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center cyber-glow">
|
||||||
|
<Bot className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<div className="animate-fade-in">
|
||||||
|
<h1 className="text-sidebar-foreground font-semibold text-lg">AI Agent</h1>
|
||||||
|
<p className="text-muted-foreground text-xs">Painel Admin</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 px-3 py-4 space-y-1">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
end={item.path === ""} // Apenas a Home precisa de match exato
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
cn(
|
||||||
|
"nav-item flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full",
|
||||||
|
isActive
|
||||||
|
? "text-primary bg-sidebar-accent font-medium"
|
||||||
|
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<item.icon className="w-5 h-5 flex-shrink-0" />
|
||||||
|
{!collapsed && <span className="animate-fade-in">{item.title}</span>}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Collapse button - desktop only */}
|
||||||
|
<div className="hidden lg:block px-3 py-4 border-t border-sidebar-border">
|
||||||
|
<button
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
className="nav-item flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full justify-center lg:justify-start text-muted-foreground hover:text-foreground hover:bg-sidebar-accent"
|
||||||
|
>
|
||||||
|
<ChevronLeft
|
||||||
|
className={cn(
|
||||||
|
"w-5 h-5 transition-transform duration-300 flex-shrink-0",
|
||||||
|
collapsed && "rotate-180"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{!collapsed && <span>Recolher</span>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(true)}
|
||||||
|
className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-lg bg-card border border-border shadow-sm"
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Mobile overlay */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<div
|
||||||
|
className="lg:hidden fixed inset-0 bg-background/80 backdrop-blur-sm z-40"
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mobile sidebar */}
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
"lg:hidden fixed left-0 top-0 h-full w-64 bg-sidebar z-50 flex flex-col border-r border-sidebar-border transition-transform duration-300",
|
||||||
|
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
className="absolute top-4 right-4 p-2 rounded-lg hover:bg-sidebar-accent"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<SidebarContent />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Desktop sidebar */}
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
"hidden lg:flex flex-col h-screen bg-sidebar border-r border-sidebar-border transition-all duration-300 sticky top-0",
|
||||||
|
collapsed ? "w-[72px]" : "w-64"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SidebarContent />
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { AppSidebar } from "./AppSidebar";
|
||||||
|
|
||||||
|
interface MainLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MainLayout({ children }: MainLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen w-full bg-background">
|
||||||
|
<AppSidebar />
|
||||||
|
<main className="flex-1 overflow-auto">
|
||||||
|
<div className="p-4 lg:p-8 pt-16 lg:pt-8">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { ptBR } from "date-fns/locale";
|
||||||
|
import { Bot, Sparkles, Zap, Brain, MessageCircle } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { GlobalFunctions } from "@/GlobalFunctions";
|
||||||
|
export default function Dashboard() {
|
||||||
|
const today = new Date();
|
||||||
|
const formattedDate = format(today, "EEEE, d 'de' MMMM 'de' yyyy", { locale: ptBR });
|
||||||
|
const hour = today.getHours();
|
||||||
|
|
||||||
|
const getGreeting = () => {
|
||||||
|
if (hour < 12) return "Bom dia";
|
||||||
|
if (hour < 18) return "Boa tarde";
|
||||||
|
return "Boa noite";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[70vh] text-center px-4">
|
||||||
|
{/* Floating particles background effect */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div className="absolute top-1/4 left-1/4 w-2 h-2 bg-primary/30 rounded-full animate-pulse" />
|
||||||
|
<div className="absolute top-1/3 right-1/3 w-1 h-1 bg-primary/40 rounded-full animate-pulse delay-300" />
|
||||||
|
<div className="absolute bottom-1/3 left-1/3 w-1.5 h-1.5 bg-primary/20 rounded-full animate-pulse delay-500" />
|
||||||
|
<div className="absolute top-1/2 right-1/4 w-1 h-1 bg-primary/30 rounded-full animate-pulse delay-700" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Agent Avatar */}
|
||||||
|
<div className="relative mb-8 animate-fade-in">
|
||||||
|
{/* Outer glow ring */}
|
||||||
|
<div className="absolute inset-0 w-32 h-32 rounded-full bg-gradient-to-r from-primary/20 via-primary/10 to-primary/20 blur-xl animate-pulse" />
|
||||||
|
|
||||||
|
{/* Middle ring */}
|
||||||
|
<div className="relative w-32 h-32 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 p-1 cyber-glow">
|
||||||
|
<div className="w-full h-full rounded-full bg-card border border-primary/30 flex items-center justify-center relative overflow-hidden">
|
||||||
|
{/* Inner gradient effect */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-primary/10 to-transparent" />
|
||||||
|
|
||||||
|
{/* Bot icon */}
|
||||||
|
<Bot className="w-14 h-14 text-primary relative z-10" />
|
||||||
|
|
||||||
|
{/* Scanning line effect */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-primary/20 to-transparent h-8 animate-[slide-scan_2s_ease-in-out_infinite]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Orbiting icons */}
|
||||||
|
<div className="absolute -top-2 -right-2 w-10 h-10 rounded-full bg-card border border-border flex items-center justify-center shadow-lg animate-bounce">
|
||||||
|
<Sparkles className="w-5 h-5 text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-1 -left-1 w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center shadow-lg animate-pulse">
|
||||||
|
<Zap className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-1/2 -right-4 w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center shadow-lg animate-pulse delay-300">
|
||||||
|
<Brain className="w-4 h-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Greeting */}
|
||||||
|
<div className="space-y-3 animate-fade-in" style={{ animationDelay: "0.2s" }}>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-foreground">
|
||||||
|
{getGreeting()}! 👋
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground">
|
||||||
|
Seu agente de IA está pronto para ajudar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date card */}
|
||||||
|
<div
|
||||||
|
className="mt-8 px-6 py-3 rounded-full bg-card/50 border border-border backdrop-blur-sm animate-fade-in"
|
||||||
|
style={{ animationDelay: "0.4s" }}
|
||||||
|
>
|
||||||
|
<p className="text-muted-foreground capitalize">
|
||||||
|
📅 {formattedDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status indicator */}
|
||||||
|
<div
|
||||||
|
className="mt-6 flex items-center gap-2 text-sm animate-fade-in"
|
||||||
|
style={{ animationDelay: "0.6s" }}
|
||||||
|
>
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">Sistema operacional</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* WhatsApp Button */}
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
className="mt-8 gap-2 border-[#25D366] text-[#25D366] hover:bg-[#25D366] hover:text-white hover:border-[#25D366] animate-fade-in"
|
||||||
|
style={{ animationDelay: "0.8s" }}
|
||||||
|
>
|
||||||
|
<a href="https://wa.me/5511914382960" target="_blank" rel="noopener noreferrer">
|
||||||
|
<MessageCircle className="w-5 h-5" />
|
||||||
|
Conversar com o Agente
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,777 @@
|
|||||||
|
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { ptBR } from "date-fns/locale";
|
||||||
|
import {
|
||||||
|
DollarSign,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
Building2,
|
||||||
|
User,
|
||||||
|
Receipt,
|
||||||
|
Filter,
|
||||||
|
Calendar,
|
||||||
|
Search,
|
||||||
|
CalendarIcon,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { personalAgent, FinancialIndicators, ExpenseItem, ExpensesResponse, ExpensesFilters, ExpenseCategory } from "@/services/personalAgent";
|
||||||
|
import { GlobalFunctions, TransferAreaProperties } from "@/GlobalFunctions";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
// Mapeamento de categoria_id para cores (cores padrão caso não tenha categoria)
|
||||||
|
const categoryColorMap: Record<number, string> = {
|
||||||
|
1: "bg-orange-500/10 text-orange-600 border-orange-500/20", // Alimentação
|
||||||
|
2: "bg-yellow-500/10 text-yellow-600 border-yellow-500/20", // Combustível
|
||||||
|
3: "bg-purple-500/10 text-purple-600 border-purple-500/20", // Hospedagem
|
||||||
|
4: "bg-blue-500/10 text-blue-600 border-blue-500/20", // Estacionamento
|
||||||
|
5: "bg-indigo-500/10 text-indigo-600 border-indigo-500/20", // Pedágio
|
||||||
|
6: "bg-cyan-500/10 text-cyan-600 border-cyan-500/20", // Transporte
|
||||||
|
};
|
||||||
|
|
||||||
|
// Função auxiliar para obter informações da categoria
|
||||||
|
const getCategoryInfo = (categoriaId: number | undefined | null, categories: ExpenseCategory[]) => {
|
||||||
|
// Se não tem categoria_id válido, retorna padrão
|
||||||
|
if (categoriaId === undefined || categoriaId === null || categoriaId === 0) {
|
||||||
|
return {
|
||||||
|
label: "Outros",
|
||||||
|
color: "bg-gray-500/10 text-gray-600 border-gray-500/20",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converte para número caso venha como string
|
||||||
|
const id = typeof categoriaId === 'string' ? parseInt(categoriaId, 10) : categoriaId;
|
||||||
|
|
||||||
|
// Busca a categoria na lista carregada
|
||||||
|
const category = categories.find(cat => cat.id === id);
|
||||||
|
|
||||||
|
// Se encontrou a categoria, usa o nome dela
|
||||||
|
if (category) {
|
||||||
|
const color = categoryColorMap[id] || "bg-gray-500/10 text-gray-600 border-gray-500/20";
|
||||||
|
return {
|
||||||
|
label: category.nome,
|
||||||
|
color: color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não encontrou mas tem ID válido, usa o ID com cor padrão
|
||||||
|
const color = categoryColorMap[id] || "bg-gray-500/10 text-gray-600 border-gray-500/20";
|
||||||
|
return {
|
||||||
|
label: `Categoria ${id}`,
|
||||||
|
color: color,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatCurrency(value: number) {
|
||||||
|
return new Intl.NumberFormat("pt-BR", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "BRL",
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(dateTimeString: string) {
|
||||||
|
// Formata "2026-01-14 10:38:57" para Date
|
||||||
|
const date = new Date(dateTimeString.replace(" ", "T"));
|
||||||
|
return {
|
||||||
|
date: date.toLocaleDateString("pt-BR"),
|
||||||
|
time: date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Financas() {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [typeFilter, setTypeFilter] = useState<string>("all");
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState<string>("all"); // Agora será o ID da categoria ou "all"
|
||||||
|
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 10;
|
||||||
|
|
||||||
|
// Estados para os indicadores financeiros da API
|
||||||
|
const [loadingIndicators, setLoadingIndicators] = useState(true);
|
||||||
|
const [indicators, setIndicators] = useState<FinancialIndicators | null>(null);
|
||||||
|
|
||||||
|
// Estados para as despesas da API
|
||||||
|
const [loadingExpenses, setLoadingExpenses] = useState(true);
|
||||||
|
const [expensesData, setExpensesData] = useState<ExpensesResponse | null>(null);
|
||||||
|
const [userEmail, setUserEmail] = useState<string>("");
|
||||||
|
|
||||||
|
// Estados para as categorias da API
|
||||||
|
const [loadingCategories, setLoadingCategories] = useState(true);
|
||||||
|
const [categories, setCategories] = useState<ExpenseCategory[]>([]);
|
||||||
|
|
||||||
|
// Carrega os indicadores financeiros da API
|
||||||
|
useEffect(() => {
|
||||||
|
const loadFinancialIndicators = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingIndicators(true);
|
||||||
|
|
||||||
|
// Obtém o email do usuário logado
|
||||||
|
let userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
// Se não tem email, tenta obter do Transfer Area como fallback
|
||||||
|
if (!userEmail) {
|
||||||
|
const transferEmail = GlobalFunctions.getTransferProperty(
|
||||||
|
TransferAreaProperties.UsuarioEmail
|
||||||
|
);
|
||||||
|
if (transferEmail) {
|
||||||
|
userEmail = transferEmail as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se ainda não tem email, tenta obter do token novamente após refresh
|
||||||
|
if (!userEmail) {
|
||||||
|
try {
|
||||||
|
await GlobalFunctions.getToken();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao fazer refresh do token:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userEmail) {
|
||||||
|
console.error("Financas: Não foi possível obter email do usuário");
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: "Email do usuário não encontrado. Por favor, faça login novamente.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setLoadingIndicators(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busca os indicadores financeiros da API
|
||||||
|
const response = await personalAgent.getFinancialIndicators(userEmail);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
setIndicators(response);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || "Erro ao carregar indicadores financeiros");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao carregar indicadores financeiros:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: errorMessage || "Erro ao carregar indicadores financeiros.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoadingIndicators(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadFinancialIndicators();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Função auxiliar para obter o email do usuário
|
||||||
|
const getUserEmail = async (): Promise<string | null> => {
|
||||||
|
let email = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
const transferEmail = GlobalFunctions.getTransferProperty(
|
||||||
|
TransferAreaProperties.UsuarioEmail
|
||||||
|
);
|
||||||
|
if (transferEmail) {
|
||||||
|
email = transferEmail as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
try {
|
||||||
|
await GlobalFunctions.getToken();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
email = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao fazer refresh do token:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return email || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carrega as despesas da API
|
||||||
|
const loadExpenses = useCallback(async (page: number = 1) => {
|
||||||
|
const emailToUse = userEmail || await getUserEmail();
|
||||||
|
if (!emailToUse) {
|
||||||
|
setLoadingExpenses(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userEmail && emailToUse) {
|
||||||
|
setUserEmail(emailToUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingExpenses(true);
|
||||||
|
|
||||||
|
// Prepara filtros para a API
|
||||||
|
const filters: ExpensesFilters = {
|
||||||
|
page,
|
||||||
|
per_page: itemsPerPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (searchTerm.trim()) {
|
||||||
|
filters.descricao = searchTerm.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryFilter !== "all") {
|
||||||
|
// Usa o ID da categoria diretamente
|
||||||
|
const categoriaId = parseInt(categoryFilter, 10);
|
||||||
|
if (!isNaN(categoriaId)) {
|
||||||
|
filters.categoria_id = categoriaId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
filters.data_inicial = format(startDate, "yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
filters.data_final = format(endDate, "yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await personalAgent.getExpenses(emailToUse, filters);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Garante que a resposta tenha estrutura válida mesmo quando não há dados
|
||||||
|
const safeResponse: ExpensesResponse = {
|
||||||
|
success: response.success,
|
||||||
|
total_registros: response.total_registros || 0,
|
||||||
|
total_paginas: response.total_paginas || 0,
|
||||||
|
per_page: response.per_page || itemsPerPage,
|
||||||
|
pagina_atual: response.pagina_atual || page,
|
||||||
|
data: Array.isArray(response.data)
|
||||||
|
? response.data.filter((item) => item && item.id && Object.keys(item).length > 0)
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
setExpensesData(safeResponse);
|
||||||
|
} else {
|
||||||
|
throw new Error("Erro ao carregar despesas");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao carregar despesas:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: errorMessage || "Erro ao carregar despesas.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoadingExpenses(false);
|
||||||
|
}
|
||||||
|
}, [userEmail, searchTerm, categoryFilter, startDate, endDate, itemsPerPage]);
|
||||||
|
|
||||||
|
// Carrega o email do usuário e as categorias na montagem do componente
|
||||||
|
useEffect(() => {
|
||||||
|
const initData = async () => {
|
||||||
|
// Carrega email do usuário
|
||||||
|
const email = await getUserEmail();
|
||||||
|
if (email) {
|
||||||
|
setUserEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carrega categorias
|
||||||
|
try {
|
||||||
|
setLoadingCategories(true);
|
||||||
|
const categoriesData = await personalAgent.getCategories();
|
||||||
|
setCategories(categoriesData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar categorias:", error);
|
||||||
|
toast({
|
||||||
|
title: "Aviso",
|
||||||
|
description: "Não foi possível carregar as categorias. Usando categorias padrão.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoadingCategories(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Debounce para busca de descrição
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (userEmail) {
|
||||||
|
setCurrentPage(1); // Reseta para página 1 ao buscar
|
||||||
|
loadExpenses(1);
|
||||||
|
}
|
||||||
|
}, 500); // Aguarda 500ms após parar de digitar
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [searchTerm, userEmail, loadExpenses]);
|
||||||
|
|
||||||
|
// Carrega despesas quando os filtros ou página mudam (exceto searchTerm que tem debounce)
|
||||||
|
useEffect(() => {
|
||||||
|
if (userEmail) {
|
||||||
|
loadExpenses(currentPage);
|
||||||
|
}
|
||||||
|
}, [currentPage, typeFilter, categoryFilter, startDate, endDate, userEmail, loadExpenses]);
|
||||||
|
|
||||||
|
// Calculate totals - usa dados da API se disponível
|
||||||
|
const totals = useMemo(() => {
|
||||||
|
if (indicators && indicators.success) {
|
||||||
|
return {
|
||||||
|
total: parseFloat(indicators.total_despesas) || 0,
|
||||||
|
corporate: parseFloat(indicators.total_corporativo) || 0,
|
||||||
|
personal: parseFloat(indicators.total_pessoal) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total: 0, personal: 0, corporate: 0 };
|
||||||
|
}, [indicators]);
|
||||||
|
|
||||||
|
// Filtra despesas por tipo (se necessário, já que a API pode não filtrar por tipo)
|
||||||
|
const filteredExpenses = useMemo(() => {
|
||||||
|
if (!expensesData || !expensesData.data || !Array.isArray(expensesData.data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtra objetos vazios ou inválidos
|
||||||
|
let expenses = expensesData.data.filter((expense) => {
|
||||||
|
// Verifica se o expense é válido e tem propriedades necessárias
|
||||||
|
// categoria_id pode ser opcional, então não validamos ele aqui
|
||||||
|
return expense &&
|
||||||
|
typeof expense === 'object' &&
|
||||||
|
expense.id &&
|
||||||
|
expense.descricao &&
|
||||||
|
expense.data_hora &&
|
||||||
|
expense.valor;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtra por tipo se necessário (a API pode não ter esse filtro)
|
||||||
|
if (typeFilter !== "all") {
|
||||||
|
expenses = expenses.filter((expense) => expense.tipo === typeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expenses;
|
||||||
|
}, [expensesData, typeFilter]);
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setSearchTerm("");
|
||||||
|
setTypeFilter("all");
|
||||||
|
setCategoryFilter("all");
|
||||||
|
setStartDate(undefined);
|
||||||
|
setEndDate(undefined);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 animate-fade-in">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold gradient-text">Finanças</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
Análise de despesas pessoais e corporativas identificadas pelo agente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Indicators */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Card className="metric-card">
|
||||||
|
<CardContent className="p-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Total de Despesas</p>
|
||||||
|
{loadingIndicators ? (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">Carregando...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-2xl font-bold mt-1">{formatCurrency(totals.total)}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||||
|
<DollarSign className="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="metric-card">
|
||||||
|
<CardContent className="p-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Despesas Corporativas</p>
|
||||||
|
{loadingIndicators ? (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">Carregando...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-2xl font-bold mt-1">{formatCurrency(totals.corporate)}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-secondary/10 flex items-center justify-center">
|
||||||
|
<Building2 className="w-6 h-6 text-secondary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="metric-card">
|
||||||
|
<CardContent className="p-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Despesas Pessoais</p>
|
||||||
|
{loadingIndicators ? (
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">Carregando...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-2xl font-bold mt-1">{formatCurrency(totals.personal)}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-warning/10 flex items-center justify-center">
|
||||||
|
<User className="w-6 h-6 text-warning" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Filter className="w-4 h-4" />
|
||||||
|
Filtros
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{/* First row: Search + Date filters */}
|
||||||
|
<div className="flex flex-col md:flex-row gap-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar por descrição..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchTerm(e.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full md:w-[160px] justify-start text-left font-normal",
|
||||||
|
!startDate && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{startDate ? format(startDate, "dd/MM/yyyy") : "Data inicial"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<CalendarComponent
|
||||||
|
mode="single"
|
||||||
|
selected={startDate}
|
||||||
|
onSelect={(date) => {
|
||||||
|
setStartDate(date);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
locale={ptBR}
|
||||||
|
initialFocus
|
||||||
|
className={cn("p-3 pointer-events-auto")}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full md:w-[160px] justify-start text-left font-normal",
|
||||||
|
!endDate && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{endDate ? format(endDate, "dd/MM/yyyy") : "Data final"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<CalendarComponent
|
||||||
|
mode="single"
|
||||||
|
selected={endDate}
|
||||||
|
onSelect={(date) => {
|
||||||
|
setEndDate(date);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
locale={ptBR}
|
||||||
|
disabled={(date) => startDate ? date < startDate : false}
|
||||||
|
initialFocus
|
||||||
|
className={cn("p-3 pointer-events-auto")}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Second row: Type + Category + Clear */}
|
||||||
|
<div className="flex flex-col md:flex-row gap-3">
|
||||||
|
<Select
|
||||||
|
value={typeFilter}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setTypeFilter(value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full md:w-[180px]">
|
||||||
|
<SelectValue placeholder="Tipo" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos os tipos</SelectItem>
|
||||||
|
<SelectItem value="pessoal">Pessoal</SelectItem>
|
||||||
|
<SelectItem value="corporativo">Corporativo</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={categoryFilter}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setCategoryFilter(value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
disabled={loadingCategories}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full md:w-[180px]">
|
||||||
|
<SelectValue placeholder={loadingCategories ? "Carregando..." : "Categoria"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todas categorias</SelectItem>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
|
{category.nome}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant="outline" onClick={clearFilters}>
|
||||||
|
Limpar filtros
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<Receipt className="w-4 h-4" />
|
||||||
|
Registro de Despesas
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[140px]">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
|
Data/Hora
|
||||||
|
</div>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Descrição</TableHead>
|
||||||
|
<TableHead className="w-[120px]">Tipo</TableHead>
|
||||||
|
<TableHead className="w-[130px]">Categoria</TableHead>
|
||||||
|
<TableHead className="w-[120px] text-right">Valor</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loadingExpenses ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-8">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Carregando despesas...</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : filteredExpenses.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||||
|
{expensesData && expensesData.total_registros === 0
|
||||||
|
? "Nenhuma despesa encontrada."
|
||||||
|
: "Nenhuma despesa encontrada com os filtros aplicados."}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredExpenses.map((expense) => {
|
||||||
|
// Validação adicional para garantir que o expense é válido
|
||||||
|
if (!expense || !expense.id || !expense.data_hora || !expense.descricao || !expense.valor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { date, time } = formatDateTime(expense.data_hora);
|
||||||
|
// Usa categoria_id diretamente (pode ser number, undefined ou null)
|
||||||
|
const categoriaId = expense.categoria_id;
|
||||||
|
const categoryInfo = getCategoryInfo(categoriaId, categories);
|
||||||
|
const valor = parseFloat(expense.valor) || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={expense.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{date}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{time}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="font-medium">{expense.descricao || "Sem descrição"}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
expense.tipo === "corporativo"
|
||||||
|
? "bg-secondary/10 text-secondary border-secondary/20"
|
||||||
|
: "bg-warning/10 text-warning border-warning/20"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{expense.tipo === "corporativo" ? (
|
||||||
|
<Building2 className="w-3 h-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<User className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{expense.tipo === "corporativo" ? "Corp." : "Pessoal"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{categoryInfo ? (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={categoryInfo.color}
|
||||||
|
>
|
||||||
|
{categoryInfo.label}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="bg-gray-500/10 text-gray-600 border-gray-500/20"
|
||||||
|
>
|
||||||
|
Sem categoria
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right font-semibold">
|
||||||
|
{formatCurrency(valor)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao renderizar despesa:", error, expense);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(Boolean) // Remove nulls do array
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{expensesData && expensesData.total_paginas > 0 && expensesData.total_paginas > 1 && (
|
||||||
|
<div className="mt-4 flex items-center justify-between">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Mostrando {(currentPage - 1) * itemsPerPage + 1} a{" "}
|
||||||
|
{Math.min(currentPage * itemsPerPage, expensesData.total_registros)} de{" "}
|
||||||
|
{expensesData.total_registros} registros
|
||||||
|
</p>
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
onClick={() => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
setCurrentPage(currentPage - 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
currentPage === 1
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
{Array.from({ length: expensesData.total_paginas }, (_, i) => i + 1).map((page) => (
|
||||||
|
<PaginationItem key={page}>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
isActive={currentPage === page}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
))}
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() => {
|
||||||
|
if (currentPage < expensesData.total_paginas) {
|
||||||
|
setCurrentPage(currentPage + 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
currentPage === expensesData.total_paginas
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Dashboard from "./Dashboard";
|
||||||
|
|
||||||
|
const Index = () => {
|
||||||
|
return <Dashboard />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { GoogleCalendarCard } from "@/modules/intelligence-ia/components/integrations/GoogleCalendarCard";
|
||||||
|
import { GoogleSheetsCard } from "@/modules/intelligence-ia/components/integrations/GoogleSheetsCard";
|
||||||
|
import { AsanaCard } from "@/modules/intelligence-ia/components/integrations/AsanaCard";
|
||||||
|
import { asanaService } from "@/services/asana";
|
||||||
|
import { personalAgent } from "@/services/personalAgent";
|
||||||
|
import { GlobalFunctions, TransferAreaProperties } from "@/GlobalFunctions";
|
||||||
|
|
||||||
|
interface Workspace {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function Integrations() {
|
||||||
|
const [asanaWorkspaces, setAsanaWorkspaces] = useState<Workspace[]>([]);
|
||||||
|
const [asanaUsers, setAsanaUsers] = useState<User[]>([]);
|
||||||
|
const [loadingAsanaWorkspaces, setLoadingAsanaWorkspaces] = useState(false);
|
||||||
|
const [loadingAsanaUsers, setLoadingAsanaUsers] = useState(false);
|
||||||
|
const [asanaToken, setAsanaToken] = useState<string>("");
|
||||||
|
const [asanaApiKey, setAsanaApiKey] = useState<string>("");
|
||||||
|
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>("");
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<string>("");
|
||||||
|
const [loadingIntegration, setLoadingIntegration] = useState(true);
|
||||||
|
const [hasIntegration, setHasIntegration] = useState(false);
|
||||||
|
const [integrationId, setIntegrationId] = useState<string>("");
|
||||||
|
const [userId, setUserId] = useState<string>("");
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const handleAsanaSave = async (apiKey: string, workspaceId: string, usuarioAsanaId: string) => {
|
||||||
|
if (!apiKey || !workspaceId || !usuarioAsanaId) {
|
||||||
|
toast.error("Preencha todos os campos obrigatórios");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encontra o nome do workspace e do usuário selecionado
|
||||||
|
const workspace = asanaWorkspaces.find(w => w.id === workspaceId);
|
||||||
|
const user = asanaUsers.find(u => u.id === usuarioAsanaId);
|
||||||
|
|
||||||
|
if (!workspace || !user) {
|
||||||
|
toast.error("Workspace ou usuário não encontrado");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
if (hasIntegration && integrationId) {
|
||||||
|
// Atualiza integração existente
|
||||||
|
await asanaService.updateIntegration(integrationId, {
|
||||||
|
api_key: apiKey,
|
||||||
|
workspace_gid: workspaceId,
|
||||||
|
workspace_nome: workspace.name,
|
||||||
|
usuario_asana_gid: usuarioAsanaId,
|
||||||
|
usuario_asana_nome: user.name,
|
||||||
|
});
|
||||||
|
toast.success("Integração do Asana atualizada com sucesso!");
|
||||||
|
} else {
|
||||||
|
// Cria nova integração
|
||||||
|
if (!userId) {
|
||||||
|
toast.error("ID do usuário não encontrado");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await asanaService.createIntegration({
|
||||||
|
user_id: userId,
|
||||||
|
api_key: apiKey,
|
||||||
|
workspace_gid: workspaceId,
|
||||||
|
workspace_nome: workspace.name,
|
||||||
|
usuario_asana_gid: usuarioAsanaId,
|
||||||
|
usuario_asana_nome: user.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.integracao_id) {
|
||||||
|
setIntegrationId(response.integracao_id);
|
||||||
|
setHasIntegration(true);
|
||||||
|
}
|
||||||
|
toast.success("Integração do Asana criada com sucesso!");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao salvar integração do Asana:", error);
|
||||||
|
const errorMessage = error && typeof error === 'object' && 'message' in error
|
||||||
|
? (error as { message: string }).message
|
||||||
|
: "Erro ao salvar integração do Asana";
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAsanaApiKeyConfirm = async (apiKey: string) => {
|
||||||
|
if (!apiKey || apiKey.trim().length === 0) {
|
||||||
|
toast.error("Insira um token válido do Asana");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingAsanaWorkspaces(true);
|
||||||
|
|
||||||
|
// Busca workspaces da API do Asana
|
||||||
|
const workspaces = await asanaService.getWorkspaces(apiKey);
|
||||||
|
|
||||||
|
if (workspaces && workspaces.length > 0) {
|
||||||
|
setAsanaWorkspaces(workspaces);
|
||||||
|
setAsanaUsers([]);
|
||||||
|
setAsanaToken(apiKey); // Armazena o token para usar na busca de usuários
|
||||||
|
toast.success(`${workspaces.length} workspace(s) encontrado(s)`);
|
||||||
|
} else {
|
||||||
|
setAsanaWorkspaces([]);
|
||||||
|
setAsanaUsers([]);
|
||||||
|
setAsanaToken("");
|
||||||
|
toast.warning("Nenhum workspace encontrado para este token");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao buscar workspaces do Asana:", error);
|
||||||
|
const errorMessage = error && typeof error === 'object' && 'message' in error
|
||||||
|
? (error as { message: string }).message
|
||||||
|
: "Erro ao buscar workspaces do Asana. Verifique se o token está correto.";
|
||||||
|
|
||||||
|
toast.error(errorMessage);
|
||||||
|
setAsanaWorkspaces([]);
|
||||||
|
setAsanaUsers([]);
|
||||||
|
setAsanaToken(""); // Limpa o token em caso de erro
|
||||||
|
} finally {
|
||||||
|
setLoadingAsanaWorkspaces(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAsanaWorkspaceChange = async (workspaceId: string) => {
|
||||||
|
setSelectedWorkspaceId(workspaceId);
|
||||||
|
|
||||||
|
if (!workspaceId || !asanaToken) {
|
||||||
|
setAsanaUsers([]);
|
||||||
|
setSelectedUserId("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingAsanaUsers(true);
|
||||||
|
|
||||||
|
// Busca usuários do workspace da API do Asana
|
||||||
|
const users = await asanaService.getUsers(asanaToken, workspaceId);
|
||||||
|
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
setAsanaUsers(users);
|
||||||
|
// Não mostra toast ao selecionar workspace (só quando confirmar token)
|
||||||
|
} else {
|
||||||
|
setAsanaUsers([]);
|
||||||
|
setSelectedUserId("");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao buscar usuários do Asana:", error);
|
||||||
|
const errorMessage = error && typeof error === 'object' && 'message' in error
|
||||||
|
? (error as { message: string }).message
|
||||||
|
: "Erro ao buscar usuários do Asana. Verifique se o token está correto.";
|
||||||
|
|
||||||
|
toast.error(errorMessage);
|
||||||
|
setAsanaUsers([]);
|
||||||
|
setSelectedUserId("");
|
||||||
|
} finally {
|
||||||
|
setLoadingAsanaUsers(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carrega a integração do Asana ao montar o componente
|
||||||
|
useEffect(() => {
|
||||||
|
const loadAsanaIntegration = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingIntegration(true);
|
||||||
|
|
||||||
|
// Obtém o ID do usuário do perfil
|
||||||
|
let userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
// Se não tem email, tenta obter do Transfer Area como fallback
|
||||||
|
if (!userEmail) {
|
||||||
|
const transferEmail = GlobalFunctions.getTransferProperty(
|
||||||
|
TransferAreaProperties.UsuarioEmail
|
||||||
|
);
|
||||||
|
if (transferEmail) {
|
||||||
|
userEmail = transferEmail as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se ainda não tem email, tenta obter do token novamente após refresh
|
||||||
|
if (!userEmail) {
|
||||||
|
try {
|
||||||
|
await GlobalFunctions.getToken();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao fazer refresh do token:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userEmail) {
|
||||||
|
setLoadingIntegration(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busca o perfil do usuário para obter o ID
|
||||||
|
const userProfile = await personalAgent.getUserProfile(userEmail);
|
||||||
|
|
||||||
|
if (!userProfile.success || !userProfile.id) {
|
||||||
|
setLoadingIntegration(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Armazena o ID do usuário para usar ao criar integração
|
||||||
|
setUserId(userProfile.id);
|
||||||
|
|
||||||
|
// Busca a integração do Asana
|
||||||
|
const integration = await asanaService.getIntegration(userProfile.id);
|
||||||
|
|
||||||
|
if (integration && integration.success) {
|
||||||
|
// Marca que há integração existente
|
||||||
|
setHasIntegration(true);
|
||||||
|
|
||||||
|
// Armazena o ID da integração (pode vir como integracao_id ou id)
|
||||||
|
if (integration.integracao_id) {
|
||||||
|
setIntegrationId(integration.integracao_id);
|
||||||
|
} else if (integration.id) {
|
||||||
|
setIntegrationId(integration.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preenche os dados da integração
|
||||||
|
if (integration.api_key) {
|
||||||
|
setAsanaApiKey(integration.api_key);
|
||||||
|
setAsanaToken(integration.api_key);
|
||||||
|
|
||||||
|
// Busca workspaces com o token
|
||||||
|
try {
|
||||||
|
const workspaces = await asanaService.getWorkspaces(integration.api_key);
|
||||||
|
setAsanaWorkspaces(workspaces);
|
||||||
|
|
||||||
|
// Se tem workspace_gid, seleciona e busca usuários
|
||||||
|
if (integration.workspace_gid) {
|
||||||
|
setSelectedWorkspaceId(integration.workspace_gid);
|
||||||
|
|
||||||
|
// Busca usuários do workspace
|
||||||
|
const users = await asanaService.getUsers(integration.api_key, integration.workspace_gid);
|
||||||
|
setAsanaUsers(users);
|
||||||
|
|
||||||
|
// Se tem usuario_asana_gid, seleciona
|
||||||
|
if (integration.usuario_asana_gid) {
|
||||||
|
setSelectedUserId(integration.usuario_asana_gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar workspaces/usuários:", error);
|
||||||
|
// Continua mesmo se não conseguir carregar workspaces
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Não há integração, marca como false
|
||||||
|
setHasIntegration(false);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao carregar integração do Asana:", error);
|
||||||
|
// Não mostra erro para o usuário, apenas deixa os campos vazios
|
||||||
|
} finally {
|
||||||
|
setLoadingIntegration(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadAsanaIntegration();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-foreground mb-1">
|
||||||
|
Integrações
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Configure as conexões do seu agente com serviços externos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Integrations Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<GoogleCalendarCard />
|
||||||
|
|
||||||
|
<GoogleSheetsCard />
|
||||||
|
|
||||||
|
<AsanaCard
|
||||||
|
apiKey={asanaApiKey}
|
||||||
|
workspaces={asanaWorkspaces}
|
||||||
|
users={asanaUsers}
|
||||||
|
selectedWorkspaceId={selectedWorkspaceId}
|
||||||
|
selectedUserId={selectedUserId}
|
||||||
|
onSave={handleAsanaSave}
|
||||||
|
onConfirmApiKey={handleAsanaApiKeyConfirm}
|
||||||
|
onWorkspaceChange={handleAsanaWorkspaceChange}
|
||||||
|
loadingWorkspaces={loadingAsanaWorkspaces}
|
||||||
|
loadingUsers={loadingAsanaUsers}
|
||||||
|
loadingIntegration={loadingIntegration}
|
||||||
|
hasIntegration={hasIntegration}
|
||||||
|
saving={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { User, Mail, Phone, Save, Bell, Loader2, Plus } from "lucide-react";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import { personalAgent, UserProfile } from "@/services/personalAgent";
|
||||||
|
import { GlobalFunctions, TransferAreaProperties } from "@/GlobalFunctions";
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
|
||||||
|
if (numbers.length <= 2) {
|
||||||
|
return numbers;
|
||||||
|
} else if (numbers.length <= 7) {
|
||||||
|
return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`;
|
||||||
|
} else if (numbers.length <= 11) {
|
||||||
|
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7)}`;
|
||||||
|
}
|
||||||
|
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7, 11)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MeuPerfil = () => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [userExists, setUserExists] = useState(false);
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [userId, setUserId] = useState<string>("");
|
||||||
|
|
||||||
|
const [nomeCompleto, setNomeCompleto] = useState("");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [whatsapp, setWhatsapp] = useState("");
|
||||||
|
const [receberLembretes, setReceberLembretes] = useState(true);
|
||||||
|
|
||||||
|
// Estados para o modal de criação
|
||||||
|
const [createNome, setCreateNome] = useState("");
|
||||||
|
const [createWhatsapp, setCreateWhatsapp] = useState("");
|
||||||
|
const [createFollowup, setCreateFollowup] = useState(true);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
|
// Verifica se o usuário está logado e carrega os dados
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuthAndLoad = async () => {
|
||||||
|
// Verifica se há token no sessionStorage primeiro
|
||||||
|
const jsonUsuario = sessionStorage.getItem('usuarioLogado');
|
||||||
|
if (!jsonUsuario) {
|
||||||
|
window.location.replace(import.meta.env.VITE_BASE_URL_HGTX_CORE || 'https://core.hgtx.com.br');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tenta obter o email do usuário logado
|
||||||
|
let userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
// Se não tem email, tenta fazer refresh do token
|
||||||
|
if (!userEmail) {
|
||||||
|
try {
|
||||||
|
await GlobalFunctions.getToken();
|
||||||
|
// Aguarda um pouco para o refresh ser processado (se necessário)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Verifica novamente após refresh
|
||||||
|
userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MeuPerfil: Erro ao obter token:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se ainda não tem email após tentar refresh, verifica se o token existe
|
||||||
|
// Se o token existe mas não tem email, pode ser problema na decodificação
|
||||||
|
if (!userEmail) {
|
||||||
|
const usuarioData = GlobalFunctions.getUsuarioLogado();
|
||||||
|
|
||||||
|
// Se não tem email mas tem token no sessionStorage, tenta carregar mesmo assim
|
||||||
|
// O loadUserProfile vai tratar o erro adequadamente se o email for necessário
|
||||||
|
// Só redireciona se o token estiver completamente inválido (sem UID e sem EID)
|
||||||
|
if (!usuarioData.email && usuarioData.UID === "0" && usuarioData.EID === "0") {
|
||||||
|
window.location.replace(import.meta.env.VITE_BASE_URL_HGTX_CORE || 'https://core.hgtx.com.br');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se passou na verificação (ou tem token válido), carrega o perfil
|
||||||
|
loadUserProfile();
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuthAndLoad();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
let userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
// Se não tem email, tenta obter do Transfer Area como fallback
|
||||||
|
if (!userEmail) {
|
||||||
|
const transferEmail = GlobalFunctions.getTransferProperty(
|
||||||
|
TransferAreaProperties.UsuarioEmail
|
||||||
|
);
|
||||||
|
if (transferEmail) {
|
||||||
|
userEmail = transferEmail as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se ainda não tem email, tenta obter do token novamente após refresh
|
||||||
|
if (!userEmail) {
|
||||||
|
try {
|
||||||
|
await GlobalFunctions.getToken();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
userEmail = GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("MeuPerfil: Erro ao fazer refresh no loadUserProfile:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userEmail) {
|
||||||
|
console.error("MeuPerfil: Não foi possível obter email do usuário");
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: "Email do usuário não encontrado. Por favor, faça login novamente.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
// Não redireciona aqui, deixa o usuário ver o erro
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEmail(userEmail);
|
||||||
|
const response = await personalAgent.getUserProfile(userEmail);
|
||||||
|
|
||||||
|
if (response.success && response.id) {
|
||||||
|
// Usuário existe
|
||||||
|
setUserExists(true);
|
||||||
|
setUserId(response.id);
|
||||||
|
setNomeCompleto(response.nome);
|
||||||
|
setWhatsapp(formatPhoneNumber(response.whatsapp));
|
||||||
|
setReceberLembretes(response.followup);
|
||||||
|
} else {
|
||||||
|
// Usuário não existe
|
||||||
|
setUserExists(false);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao carregar perfil:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
if (errorMessage.includes("não está cadastrado")) {
|
||||||
|
setUserExists(false);
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: errorMessage || "Erro ao carregar perfil do usuário.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWhatsappChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setWhatsapp(formatted);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateWhatsappChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setCreateWhatsapp(formatted);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!userExists || !userId) {
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: "Usuário não encontrado. Por favor, crie uma conta primeiro.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
const response = await personalAgent.updateUser(userId, {
|
||||||
|
nome: nomeCompleto,
|
||||||
|
whatsapp: whatsapp,
|
||||||
|
followup: receberLembretes,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast({
|
||||||
|
title: "Perfil atualizado",
|
||||||
|
description: "Suas informações foram salvas com sucesso.",
|
||||||
|
});
|
||||||
|
// Atualiza os dados locais
|
||||||
|
setNomeCompleto(response.nome);
|
||||||
|
setWhatsapp(formatPhoneNumber(response.whatsapp));
|
||||||
|
setReceberLembretes(response.followup);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || "Erro ao atualizar perfil");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao salvar:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
toast({
|
||||||
|
title: "Erro ao salvar",
|
||||||
|
description: errorMessage || "Erro ao atualizar perfil. Tente novamente.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateUser = async () => {
|
||||||
|
if (!createNome.trim()) {
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: "Nome é obrigatório.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createWhatsapp.trim()) {
|
||||||
|
toast({
|
||||||
|
title: "Erro",
|
||||||
|
description: "WhatsApp é obrigatório.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCreating(true);
|
||||||
|
const response = await personalAgent.createUser({
|
||||||
|
nome: createNome.trim(),
|
||||||
|
email: email,
|
||||||
|
whatsapp: createWhatsapp,
|
||||||
|
followup: createFollowup,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.id) {
|
||||||
|
toast({
|
||||||
|
title: "Conta criada",
|
||||||
|
description: "Sua conta foi criada com sucesso!",
|
||||||
|
});
|
||||||
|
setUserExists(true);
|
||||||
|
setUserId(response.id);
|
||||||
|
setNomeCompleto(response.nome);
|
||||||
|
setWhatsapp(formatPhoneNumber(response.whatsapp));
|
||||||
|
setReceberLembretes(response.followup);
|
||||||
|
setShowCreateModal(false);
|
||||||
|
// Limpa os campos do modal
|
||||||
|
setCreateNome("");
|
||||||
|
setCreateWhatsapp("");
|
||||||
|
setCreateFollowup(true);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || "Erro ao criar conta");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error("Erro ao criar usuário:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
toast({
|
||||||
|
title: "Erro ao criar conta",
|
||||||
|
description: errorMessage || "Erro ao criar conta. Tente novamente.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||||
|
<p className="text-muted-foreground">Carregando perfil...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Meu Perfil</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
Gerencie suas informações pessoais
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!userExists ? (
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<CardContent className="py-12 text-center">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<User className="w-16 h-16 mx-auto text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">
|
||||||
|
Você ainda não possui uma conta
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-6">
|
||||||
|
Crie sua conta para começar a usar o agente pessoal de IA
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setShowCreateModal(true)} size="lg">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Criar Conta
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card className="max-w-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
Informações Pessoais
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="nomeCompleto" className="flex items-center gap-2">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
Nome Completo
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="nomeCompleto"
|
||||||
|
value={nomeCompleto}
|
||||||
|
onChange={(e) => setNomeCompleto(e.target.value)}
|
||||||
|
placeholder="Digite seu nome completo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="flex items-center gap-2">
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
|
E-mail
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
readOnly
|
||||||
|
className="bg-muted cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
O e-mail não pode ser alterado
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="whatsapp" className="flex items-center gap-2">
|
||||||
|
<Phone className="w-4 h-4" />
|
||||||
|
WhatsApp
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="whatsapp"
|
||||||
|
value={whatsapp}
|
||||||
|
onChange={handleWhatsappChange}
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
maxLength={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="receberLembretes" className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<Bell className="w-4 h-4" />
|
||||||
|
Receber Follow up e lembretes
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Receba notificações sobre eventos e tarefas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="receberLembretes"
|
||||||
|
checked={receberLembretes}
|
||||||
|
onCheckedChange={setReceberLembretes}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Salvando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
Salvar Alterações
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Criação de Conta */}
|
||||||
|
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Criar Conta</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Preencha os dados abaixo para criar sua conta no agente pessoal de IA
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="createNome">Nome Completo</Label>
|
||||||
|
<Input
|
||||||
|
id="createNome"
|
||||||
|
value={createNome}
|
||||||
|
onChange={(e) => setCreateNome(e.target.value)}
|
||||||
|
placeholder="Digite seu nome completo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="createEmail">E-mail</Label>
|
||||||
|
<Input
|
||||||
|
id="createEmail"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
readOnly
|
||||||
|
className="bg-muted cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
O e-mail é baseado na sua conta logada
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="createWhatsapp">WhatsApp</Label>
|
||||||
|
<Input
|
||||||
|
id="createWhatsapp"
|
||||||
|
value={createWhatsapp}
|
||||||
|
onChange={handleCreateWhatsappChange}
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
maxLength={15}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="createFollowup" className="cursor-pointer">
|
||||||
|
Receber Follow up e lembretes
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Receba notificações sobre eventos e tarefas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="createFollowup"
|
||||||
|
checked={createFollowup}
|
||||||
|
onCheckedChange={setCreateFollowup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
disabled={creating}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateUser} disabled={creating}>
|
||||||
|
{creating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Criando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Criar Conta
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MeuPerfil;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-muted">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="mb-4 text-4xl font-bold">404</h1>
|
||||||
|
<p className="mb-4 text-xl text-muted-foreground">Oops! Page not found</p>
|
||||||
|
<a href="/" className="text-primary underline hover:text-primary/90">
|
||||||
|
Return to Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
export interface AsanaWorkspaceResponse {
|
||||||
|
gid: string;
|
||||||
|
resource_type: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsanaWorkspacesResponse {
|
||||||
|
data: AsanaWorkspaceResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsanaWorkspace {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsanaUserResponse {
|
||||||
|
gid: string;
|
||||||
|
name: string;
|
||||||
|
resource_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsanaUsersResponse {
|
||||||
|
data: AsanaUserResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsanaUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface AsanaIntegrationResponse {
|
||||||
|
success: boolean;
|
||||||
|
integracao_id?: string;
|
||||||
|
id?: string;
|
||||||
|
api_key?: string;
|
||||||
|
workspace_gid?: string;
|
||||||
|
workspace_nome?: string;
|
||||||
|
usuario_asana_gid?: string;
|
||||||
|
usuario_asana_nome?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsanaIntegrationRequest {
|
||||||
|
user_id?: string;
|
||||||
|
api_key: string;
|
||||||
|
workspace_gid: string;
|
||||||
|
workspace_nome: string;
|
||||||
|
usuario_asana_gid: string;
|
||||||
|
usuario_asana_nome: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsanaService {
|
||||||
|
private readonly ASANA_BASE_URL = 'https://app.asana.com/api/1.0';
|
||||||
|
|
||||||
|
async getWorkspaces(token: string): Promise<AsanaWorkspace[]> {
|
||||||
|
if (!token || token.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Token do Asana é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'authorization': `Bearer ${token.trim()}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get<AsanaWorkspacesResponse>(
|
||||||
|
`${this.ASANA_BASE_URL}/workspaces`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data.data.map((workspace) => ({
|
||||||
|
id: workspace.gid,
|
||||||
|
name: workspace.name,
|
||||||
|
}));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Erro ao buscar workspaces do Asana:', error);
|
||||||
|
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as { response?: { data?: any; status?: number } };
|
||||||
|
|
||||||
|
if (axiosError.response?.status === 401) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Token inválido ou expirado. Verifique sua chave de API.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axiosError.response?.data) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: axiosError.response.data.message || 'Erro ao buscar workspaces do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Erro ao buscar workspaces do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsers(token: string, workspaceId: string): Promise<AsanaUser[]> {
|
||||||
|
if (!token || token.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Token do Asana é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workspaceId || workspaceId.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'ID do workspace é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'authorization': `Bearer ${token.trim()}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get<AsanaUsersResponse>(
|
||||||
|
`${this.ASANA_BASE_URL}/users?workspace=${workspaceId}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Converte a resposta da API para o formato esperado pelo componente
|
||||||
|
// A API retorna gid, mas o componente espera id
|
||||||
|
return response.data.data.map((user) => ({
|
||||||
|
id: user.gid,
|
||||||
|
name: user.name,
|
||||||
|
}));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Erro ao buscar usuários do Asana:', error);
|
||||||
|
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as { response?: { data?: any; status?: number } };
|
||||||
|
|
||||||
|
if (axiosError.response?.status === 401) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Token inválido ou expirado. Verifique sua chave de API.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axiosError.response?.data) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: axiosError.response.data.message || 'Erro ao buscar usuários do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Erro ao buscar usuários do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIntegration(userId: string): Promise<AsanaIntegrationResponse | null> {
|
||||||
|
if (!userId || userId.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'ID do usuário é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
const { GlobalFunctions } = await import('@/GlobalFunctions');
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'accept': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<AsanaIntegrationResponse>(
|
||||||
|
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/c898beff-84cb-44df-a69c-6eff27ccd7aa/codex/agente-pessoal/integracoes/asana/${userId}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.data.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Erro ao buscar integração do Asana:', error);
|
||||||
|
|
||||||
|
// Se o erro for 404 ou similar, significa que não tem integração cadastrada
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as { response?: { status?: number } };
|
||||||
|
if (axiosError.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createIntegration(request: AsanaIntegrationRequest): Promise<AsanaIntegrationResponse> {
|
||||||
|
if (!request.user_id || !request.api_key || !request.workspace_gid || !request.usuario_asana_gid) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Dados incompletos para criar integração',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
const { GlobalFunctions } = await import('@/GlobalFunctions');
|
||||||
|
|
||||||
|
// Obtém o token JWT para autenticação
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'accept': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adiciona API key (obrigatória)
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adiciona token JWT se disponível
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post<AsanaIntegrationResponse>(
|
||||||
|
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/integracoes/asana`,
|
||||||
|
request,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Erro ao criar integração do Asana:', error);
|
||||||
|
|
||||||
|
// Retorna o erro da API se existir
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as { response?: { data?: any; status?: number } };
|
||||||
|
|
||||||
|
if (axiosError.response?.data) {
|
||||||
|
const errorData = axiosError.response.data;
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: errorData.message || 'Erro ao criar integração do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Erro ao criar integração do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIntegration(integracaoId: string, request: Omit<AsanaIntegrationRequest, 'user_id'>): Promise<AsanaIntegrationResponse> {
|
||||||
|
if (!integracaoId || !request.api_key || !request.workspace_gid || !request.usuario_asana_gid) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Dados incompletos para atualizar integração',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
const { GlobalFunctions } = await import('@/GlobalFunctions');
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'accept': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/c898beff-84cb-44df-a69c-6eff27ccd7aa/codex/agente-pessoal/integracoes/asana/${integracaoId}`;
|
||||||
|
|
||||||
|
const response = await axios.post<AsanaIntegrationResponse>(
|
||||||
|
url,
|
||||||
|
request,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Erro ao atualizar integração do Asana:', error);
|
||||||
|
|
||||||
|
// Retorna o erro da API se existir
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as { response?: { data?: any; status?: number } };
|
||||||
|
|
||||||
|
if (axiosError.response?.data) {
|
||||||
|
const errorData = axiosError.response.data;
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: errorData.message || 'Erro ao atualizar integração do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Erro ao atualizar integração do Asana',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporta instância única (Singleton)
|
||||||
|
export const asanaService = new AsanaService();
|
||||||
@@ -6,7 +6,28 @@ export { apiService } from './api';
|
|||||||
export { transcriptionService } from './transcription';
|
export { transcriptionService } from './transcription';
|
||||||
export { audioGenerationService, VOICE_OPTIONS } from './audioGeneration';
|
export { audioGenerationService, VOICE_OPTIONS } from './audioGeneration';
|
||||||
export { imageGenerationService, IMAGE_SIZE_OPTIONS } from './imageGeneration';
|
export { imageGenerationService, IMAGE_SIZE_OPTIONS } from './imageGeneration';
|
||||||
|
export { personalAgent } from './personalAgent';
|
||||||
|
export { asanaService } from './asana';
|
||||||
export type { TranscriptionResponse, TranscriptionRequest } from './transcription';
|
export type { TranscriptionResponse, TranscriptionRequest } from './transcription';
|
||||||
export type { AudioGenerationResponse, AudioGenerationRequest, VoiceType } from './audioGeneration';
|
export type { AudioGenerationResponse, AudioGenerationRequest, VoiceType } from './audioGeneration';
|
||||||
export type { ImageGenerationResponse, ImageGenerationRequest, ImageSize } from './imageGeneration';
|
export type { ImageGenerationResponse, ImageGenerationRequest, ImageSize } from './imageGeneration';
|
||||||
|
export type {
|
||||||
|
UserProfile,
|
||||||
|
CreateUserRequest,
|
||||||
|
UpdateUserRequest,
|
||||||
|
FinancialIndicators,
|
||||||
|
ExpenseItem,
|
||||||
|
ExpensesResponse,
|
||||||
|
ExpensesFilters,
|
||||||
|
ExpenseCategory
|
||||||
|
} from './personalAgent';
|
||||||
|
export type {
|
||||||
|
AsanaWorkspace,
|
||||||
|
AsanaWorkspaceResponse,
|
||||||
|
AsanaWorkspacesResponse,
|
||||||
|
AsanaUser,
|
||||||
|
AsanaUserResponse,
|
||||||
|
AsanaUsersResponse,
|
||||||
|
AsanaIntegrationResponse
|
||||||
|
} from './asana';
|
||||||
export type { ApiResponse, UserConfig, ApiError } from './types';
|
export type { ApiResponse, UserConfig, ApiError } from './types';
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
import { GlobalFunctions } from '@/GlobalFunctions';
|
||||||
|
export interface UserProfile {
|
||||||
|
success: boolean;
|
||||||
|
id?: string;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
whatsapp: string;
|
||||||
|
followup: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserRequest {
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
whatsapp: string;
|
||||||
|
followup: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateUserRequest {
|
||||||
|
nome: string;
|
||||||
|
whatsapp: string;
|
||||||
|
followup: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinancialIndicators {
|
||||||
|
success: boolean;
|
||||||
|
id: string;
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
total_despesas: string;
|
||||||
|
total_corporativo: string;
|
||||||
|
total_pessoal: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpenseItem {
|
||||||
|
id: string;
|
||||||
|
data_hora: string;
|
||||||
|
descricao: string;
|
||||||
|
tipo: "pessoal" | "corporativo";
|
||||||
|
valor: string;
|
||||||
|
categoria_id: number;
|
||||||
|
categoria_nome: string;
|
||||||
|
usuario_nome: string;
|
||||||
|
usuario_email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpensesResponse {
|
||||||
|
success: boolean;
|
||||||
|
total_registros: number;
|
||||||
|
total_paginas: number;
|
||||||
|
per_page: number;
|
||||||
|
pagina_atual: number;
|
||||||
|
data: ExpenseItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpensesFilters {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
descricao?: string;
|
||||||
|
categoria_id?: number;
|
||||||
|
data_inicial?: string;
|
||||||
|
data_final?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpenseCategory {
|
||||||
|
id: number;
|
||||||
|
nome: string;
|
||||||
|
descricao: string;
|
||||||
|
criado_em: string;
|
||||||
|
atualizado_em: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PersonalAgent {
|
||||||
|
|
||||||
|
async getUserProfile(userEmail?: string): Promise<UserProfile> {
|
||||||
|
const email = userEmail || GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email do usuário não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<UserProfile>(
|
||||||
|
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/user/${email}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.data) {
|
||||||
|
return error.response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao buscar perfil do usuário',
|
||||||
|
status: error.response?.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(request: CreateUserRequest): Promise<UserProfile> {
|
||||||
|
|
||||||
|
if (!request.nome || request.nome.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Nome é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.email || request.email.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.whatsapp || request.whatsapp.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'WhatsApp é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const whatsappNumbers = request.whatsapp.replace(/\D/g, '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
} else {
|
||||||
|
console.warn('API_KEY não configurada');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post<UserProfile>(
|
||||||
|
'https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/user',
|
||||||
|
{
|
||||||
|
nome: request.nome.trim(),
|
||||||
|
email: request.email.trim(),
|
||||||
|
whatsapp: whatsappNumbers,
|
||||||
|
followup: request.followup,
|
||||||
|
},
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.data) {
|
||||||
|
throw error.response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao criar usuário',
|
||||||
|
status: error.response?.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser(userId: string, request: UpdateUserRequest): Promise<UserProfile> {
|
||||||
|
|
||||||
|
if (!userId || userId.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'ID do usuário é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.nome || request.nome.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Nome é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.whatsapp || request.whatsapp.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'WhatsApp é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const whatsappNumbers = request.whatsapp.replace(/\D/g, '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post<UserProfile>(
|
||||||
|
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/user/${userId}`,
|
||||||
|
{
|
||||||
|
nome: request.nome.trim(),
|
||||||
|
whatsapp: whatsappNumbers,
|
||||||
|
followup: request.followup,
|
||||||
|
},
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.data) {
|
||||||
|
throw error.response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao atualizar usuário',
|
||||||
|
status: error.response?.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFinancialIndicators(userEmail?: string): Promise<FinancialIndicators> {
|
||||||
|
const email = userEmail || GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email do usuário é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<FinancialIndicators>(
|
||||||
|
`https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/financas/indicadores/${email}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as { response?: { data?: FinancialIndicators } };
|
||||||
|
if (axiosError.response?.data) {
|
||||||
|
throw axiosError.response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Erro ao buscar indicadores financeiros',
|
||||||
|
status: error && typeof error === 'object' && 'response' in error
|
||||||
|
? (error as { response?: { status?: number } }).response?.status
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getExpenses(userEmail?: string, filters?: ExpensesFilters): Promise<ExpensesResponse> {
|
||||||
|
const email = userEmail || GlobalFunctions.getUsuarioLogado().email;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email do usuário é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constrói query params
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters?.page) params.append('page', filters.page.toString());
|
||||||
|
if (filters?.per_page) params.append('per_page', filters.per_page.toString());
|
||||||
|
if (filters?.descricao) params.append('descricao', filters.descricao);
|
||||||
|
if (filters?.categoria_id) params.append('categoria_id', filters.categoria_id.toString());
|
||||||
|
if (filters?.data_inicial) params.append('data_inicial', filters.data_inicial);
|
||||||
|
if (filters?.data_final) params.append('data_final', filters.data_final);
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/2299acaf-70ee-47e3-a6fc-56dcc678d651/codex/agente-pessoal/financas/${email}${queryString ? `?${queryString}` : ''}`;
|
||||||
|
|
||||||
|
const response = await axios.get<ExpensesResponse[]>(url, { headers });
|
||||||
|
|
||||||
|
if (Array.isArray(response.data) && response.data.length > 0) {
|
||||||
|
const expensesResponse = response.data[0];
|
||||||
|
|
||||||
|
// Valida e limpa o array de dados, removendo objetos vazios
|
||||||
|
if (expensesResponse.data && Array.isArray(expensesResponse.data)) {
|
||||||
|
// Filtra objetos vazios (sem propriedades ou apenas com propriedades vazias)
|
||||||
|
expensesResponse.data = expensesResponse.data.filter((item) => {
|
||||||
|
// Verifica se o objeto tem pelo menos uma propriedade válida
|
||||||
|
return item && typeof item === 'object' && Object.keys(item).length > 0 && item.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Se após filtrar não há dados, garante que data seja um array vazio
|
||||||
|
if (expensesResponse.data.length === 0) {
|
||||||
|
expensesResponse.data = [];
|
||||||
|
expensesResponse.total_registros = 0;
|
||||||
|
expensesResponse.total_paginas = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Se data não é um array válido, inicializa como array vazio
|
||||||
|
expensesResponse.data = [];
|
||||||
|
expensesResponse.total_registros = 0;
|
||||||
|
expensesResponse.total_paginas = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expensesResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback caso a estrutura seja diferente - retorna resposta vazia
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
total_registros: 0,
|
||||||
|
total_paginas: 0,
|
||||||
|
per_page: filters?.per_page || 10,
|
||||||
|
pagina_atual: filters?.page || 1,
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Retorna o erro da API se existir
|
||||||
|
if (error && typeof error === 'object' && 'response' in error) {
|
||||||
|
const axiosError = error as { response?: { data?: ExpensesResponse } };
|
||||||
|
if (axiosError.response?.data) {
|
||||||
|
throw axiosError.response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : 'Erro ao buscar despesas',
|
||||||
|
status: error && typeof error === 'object' && 'response' in error
|
||||||
|
? (error as { response?: { status?: number } }).response?.status
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategories(): Promise<ExpenseCategory[]> {
|
||||||
|
try {
|
||||||
|
const axios = (await import('axios')).default;
|
||||||
|
|
||||||
|
const token = await GlobalFunctions.getToken();
|
||||||
|
const apiKey = import.meta.env.VITE_API_KEY || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['apikey'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get<ExpenseCategory[]>('https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/agente-pessoal/categorias', { headers });
|
||||||
|
|
||||||
|
// A API retorna um array de categorias
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Erro ao buscar categorias:', error);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const personalAgent = new PersonalAgent();
|
||||||
@@ -34,6 +34,14 @@ export default {
|
|||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
|
success: {
|
||||||
|
DEFAULT: "hsl(var(--success))",
|
||||||
|
foreground: "hsl(var(--success-foreground))",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
DEFAULT: "hsl(var(--warning))",
|
||||||
|
foreground: "hsl(var(--warning-foreground))",
|
||||||
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ export default defineConfig(({ mode }) => ({
|
|||||||
server: {
|
server: {
|
||||||
host: "::",
|
host: "::",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
|
proxy: {
|
||||||
|
'/api/intelligence': {
|
||||||
|
target: 'https://prod-hgtx-intelligence-n8n.hgtx.com.br',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api\/intelligence/, ''),
|
||||||
|
secure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
|
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Reference in New Issue
Block a user