diff --git a/package-lock.json b/package-lock.json index 6a27f24..6c0e685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2911,6 +2911,7 @@ "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2928,6 +2929,7 @@ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2939,6 +2941,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2989,6 +2992,7 @@ "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", @@ -3221,6 +3225,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3442,6 +3447,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -3800,6 +3806,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -3904,7 +3911,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4047,6 +4055,7 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -5742,6 +5751,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5934,6 +5944,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5960,6 +5971,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5973,6 +5985,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.61.1.tgz", "integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6526,6 +6539,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6650,6 +6664,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6829,6 +6844,7 @@ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/App.tsx b/src/App.tsx index bfe5e7b..d5f4ec3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import NotFound from "./pages/NotFound"; import Redirect from "./pages/Redirect"; import { GlobalFunctions } from "./GlobalFunctions"; import React from "react"; +import IntelligenceIAApp from "./modules/intelligence-ia/App"; const queryClient = new QueryClient(); @@ -25,8 +26,11 @@ const App = () => ( element={} /> + {/* Módulo Intelligence IA como subpath*/} + } /> + } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} + } /> } /> diff --git a/src/index.css b/src/index.css index 388ac45..069a398 100644 --- a/src/index.css +++ b/src/index.css @@ -31,6 +31,12 @@ --destructive: 0 84% 60%; --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%; --input: 214 32% 91%; --ring: 190 100% 45%; @@ -84,6 +90,12 @@ --destructive: 0 84% 60%; --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%; --input: 222 30% 18%; --ring: 190 100% 50%; @@ -137,7 +149,7 @@ @layer components { .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 { @@ -152,4 +164,63 @@ .gradient-text { @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; + } } diff --git a/src/modules/intelligence-ia/App.tsx b/src/modules/intelligence-ia/App.tsx new file mode 100644 index 0000000..116baa9 --- /dev/null +++ b/src/modules/intelligence-ia/App.tsx @@ -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 ( + + + } /> + } /> + } /> + } /> + } /> + + + ); +}; + +export default IntelligenceIAApp; diff --git a/src/modules/intelligence-ia/assets/asana-logo.png b/src/modules/intelligence-ia/assets/asana-logo.png new file mode 100644 index 0000000..c76e1ad Binary files /dev/null and b/src/modules/intelligence-ia/assets/asana-logo.png differ diff --git a/src/modules/intelligence-ia/assets/google-calendar-logo.png b/src/modules/intelligence-ia/assets/google-calendar-logo.png new file mode 100644 index 0000000..26cbd30 Binary files /dev/null and b/src/modules/intelligence-ia/assets/google-calendar-logo.png differ diff --git a/src/modules/intelligence-ia/components/NavLink.tsx b/src/modules/intelligence-ia/components/NavLink.tsx new file mode 100644 index 0000000..a561a95 --- /dev/null +++ b/src/modules/intelligence-ia/components/NavLink.tsx @@ -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 { + className?: string; + activeClassName?: string; + pendingClassName?: string; +} + +const NavLink = forwardRef( + ({ className, activeClassName, pendingClassName, to, ...props }, ref) => { + return ( + + cn(className, isActive && activeClassName, isPending && pendingClassName) + } + {...props} + /> + ); + }, +); + +NavLink.displayName = "NavLink"; + +export { NavLink }; diff --git a/src/modules/intelligence-ia/components/dashboard/MetricCard.tsx b/src/modules/intelligence-ia/components/dashboard/MetricCard.tsx new file mode 100644 index 0000000..7ac4e8f --- /dev/null +++ b/src/modules/intelligence-ia/components/dashboard/MetricCard.tsx @@ -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 ( +
+
+
+ {icon} +
+ {status && ( + + )} +
+

{title}

+

{value}

+ {subtitle && ( +

{subtitle}

+ )} +
+ ); +} diff --git a/src/modules/intelligence-ia/components/dashboard/StatusBadge.tsx b/src/modules/intelligence-ia/components/dashboard/StatusBadge.tsx new file mode 100644 index 0000000..2f8ffef --- /dev/null +++ b/src/modules/intelligence-ia/components/dashboard/StatusBadge.tsx @@ -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 ( + + + {label || config.label} + + ); +} diff --git a/src/modules/intelligence-ia/components/integrations/AsanaCard.tsx b/src/modules/intelligence-ia/components/integrations/AsanaCard.tsx new file mode 100644 index 0000000..40ee0a6 --- /dev/null +++ b/src/modules/intelligence-ia/components/integrations/AsanaCard.tsx @@ -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; + onConfirmApiKey?: (apiKey: string) => void | Promise; + onWorkspaceChange?: (workspaceId: string) => void | Promise; +} + +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 ( +
+
+
+ Asana +
+
+

Asana

+

Gerencie tarefas e projetos

+
+
+ +
+
+ +
+ {loadingIntegration ? ( +
+ +
+ +
+
+ ) : ( +
+
+ handleKeyChange(e.target.value)} + placeholder="Insira sua chave de API" + className="pr-10 bg-card border-border" + disabled={loadingWorkspaces} + /> + +
+ +
+ )} +
+
+ +
+ + {loadingIntegration ? ( +
+ +
+ + Carregando integração... +
+
+ ) : ( +
+ + {loadingWorkspaces && ( +
+ +
+ )} +
+ )} +
+ +
+ + {loadingUsers ? ( +
+ +
+ + Carregando usuários... +
+
+ ) : ( +
+ +
+ )} +
+ +
+ +
+ +
+
+ ); +} diff --git a/src/modules/intelligence-ia/components/integrations/GoogleCalendarCard.tsx b/src/modules/intelligence-ia/components/integrations/GoogleCalendarCard.tsx new file mode 100644 index 0000000..fceabff --- /dev/null +++ b/src/modules/intelligence-ia/components/integrations/GoogleCalendarCard.tsx @@ -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 ( +
+
+
+ Google Calendar +
+
+
+

Google Calendar

+ + Em breve + +
+

Sincronize reuniões e eventos

+
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ ); +} diff --git a/src/modules/intelligence-ia/components/integrations/GoogleSheetsCard.tsx b/src/modules/intelligence-ia/components/integrations/GoogleSheetsCard.tsx new file mode 100644 index 0000000..67f0aa8 --- /dev/null +++ b/src/modules/intelligence-ia/components/integrations/GoogleSheetsCard.tsx @@ -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 ( +
+
+
+ +
+
+
+

Google Sheets

+ + Em breve + +
+

Gerencie planilhas e dados

+
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ ); +} diff --git a/src/modules/intelligence-ia/components/layout/AppSidebar.tsx b/src/modules/intelligence-ia/components/layout/AppSidebar.tsx new file mode 100644 index 0000000..dfdb86c --- /dev/null +++ b/src/modules/intelligence-ia/components/layout/AppSidebar.tsx @@ -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 */} +
+
+ +
+ {!collapsed && ( +
+

AI Agent

+

Painel Admin

+
+ )} +
+ + {/* Navigation */} + + + {/* Collapse button - desktop only */} +
+ +
+ + ); + + return ( + <> + {/* Mobile menu button */} + + + {/* Mobile overlay */} + {mobileOpen && ( +
setMobileOpen(false)} + /> + )} + + {/* Mobile sidebar */} + + + {/* Desktop sidebar */} + + + ); +} diff --git a/src/modules/intelligence-ia/components/layout/MainLayout.tsx b/src/modules/intelligence-ia/components/layout/MainLayout.tsx new file mode 100644 index 0000000..7444873 --- /dev/null +++ b/src/modules/intelligence-ia/components/layout/MainLayout.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; +import { AppSidebar } from "./AppSidebar"; + +interface MainLayoutProps { + children: ReactNode; +} + +export function MainLayout({ children }: MainLayoutProps) { + return ( +
+ +
+
+ {children} +
+
+
+ ); +} diff --git a/src/modules/intelligence-ia/pages/Dashboard.tsx b/src/modules/intelligence-ia/pages/Dashboard.tsx new file mode 100644 index 0000000..d0285ea --- /dev/null +++ b/src/modules/intelligence-ia/pages/Dashboard.tsx @@ -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 ( +
+ {/* Floating particles background effect */} +
+
+
+
+
+
+ + {/* AI Agent Avatar */} +
+ {/* Outer glow ring */} +
+ + {/* Middle ring */} +
+
+ {/* Inner gradient effect */} +
+ + {/* Bot icon */} + + + {/* Scanning line effect */} +
+
+
+ + {/* Orbiting icons */} +
+ +
+
+ +
+
+ +
+
+ + {/* Greeting */} +
+

+ {getGreeting()}! 👋 +

+

+ Seu agente de IA está pronto para ajudar +

+
+ + {/* Date card */} +
+

+ 📅 {formattedDate} +

+
+ + {/* Status indicator */} +
+ + + + + Sistema operacional +
+ + {/* WhatsApp Button */} + +
+ ); +} + + + diff --git a/src/modules/intelligence-ia/pages/Financas.tsx b/src/modules/intelligence-ia/pages/Financas.tsx new file mode 100644 index 0000000..8a521cd --- /dev/null +++ b/src/modules/intelligence-ia/pages/Financas.tsx @@ -0,0 +1,778 @@ +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 { userProfileService, FinancialIndicators, ExpenseItem, ExpensesResponse, ExpensesFilters, ExpenseCategory } from "@/services/userProfile"; +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 = { + 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("all"); + const [categoryFilter, setCategoryFilter] = useState("all"); // Agora será o ID da categoria ou "all" + const [startDate, setStartDate] = useState(undefined); + const [endDate, setEndDate] = useState(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(null); + + // Estados para as despesas da API + const [loadingExpenses, setLoadingExpenses] = useState(true); + const [expensesData, setExpensesData] = useState(null); + const [userEmail, setUserEmail] = useState(""); + + // Estados para as categorias da API + const [loadingCategories, setLoadingCategories] = useState(true); + const [categories, setCategories] = useState([]); + + // 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 userProfileService.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 => { + 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 userProfileService.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 userProfileService.getCategories(); + console.log("Categorias carregadas:", categoriesData); + 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 ( +
+ {/* Header */} +
+

Finanças

+

+ Análise de despesas pessoais e corporativas identificadas pelo agente +

+
+ + {/* Indicators */} +
+ + +
+
+

Total de Despesas

+ {loadingIndicators ? ( +
+ + Carregando... +
+ ) : ( + <> +

{formatCurrency(totals.total)}

+ + )} +
+
+ +
+
+
+
+ + + +
+
+

Despesas Corporativas

+ {loadingIndicators ? ( +
+ + Carregando... +
+ ) : ( + <> +

{formatCurrency(totals.corporate)}

+ + )} +
+
+ +
+
+
+
+ + + +
+
+

Despesas Pessoais

+ {loadingIndicators ? ( +
+ + Carregando... +
+ ) : ( + <> +

{formatCurrency(totals.personal)}

+ + )} +
+
+ +
+
+
+
+
+ + {/* Filters */} + + + + + Filtros + + + +
+ {/* First row: Search + Date filters */} +
+
+
+ + { + setSearchTerm(e.target.value); + setCurrentPage(1); + }} + className="pl-9" + /> +
+
+ + + + + + { + setStartDate(date); + setCurrentPage(1); + }} + locale={ptBR} + initialFocus + className={cn("p-3 pointer-events-auto")} + /> + + + + + + + + { + setEndDate(date); + setCurrentPage(1); + }} + locale={ptBR} + disabled={(date) => startDate ? date < startDate : false} + initialFocus + className={cn("p-3 pointer-events-auto")} + /> + + +
+ + {/* Second row: Type + Category + Clear */} +
+ + + +
+
+
+
+ + {/* Table */} + + + + + Registro de Despesas + + + + + + + +
+ + Data/Hora +
+
+ Descrição + Tipo + Categoria + Valor +
+
+ + {loadingExpenses ? ( + + +
+ + Carregando despesas... +
+
+
+ ) : filteredExpenses.length === 0 ? ( + + + {expensesData && expensesData.total_registros === 0 + ? "Nenhuma despesa encontrada." + : "Nenhuma despesa encontrada com os filtros aplicados."} + + + ) : ( + 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 ( + + +
+ {date} + {time} +
+
+ + {expense.descricao || "Sem descrição"} + + + + {expense.tipo === "corporativo" ? ( + + ) : ( + + )} + {expense.tipo === "corporativo" ? "Corp." : "Pessoal"} + + + + {categoryInfo ? ( + + {categoryInfo.label} + + ) : ( + + Sem categoria + + )} + + + {formatCurrency(valor)} + +
+ ); + } catch (error) { + console.error("Erro ao renderizar despesa:", error, expense); + return null; + } + }).filter(Boolean) // Remove nulls do array + )} +
+
+ + {/* Pagination */} + {expensesData && expensesData.total_paginas > 0 && expensesData.total_paginas > 1 && ( +
+

+ Mostrando {(currentPage - 1) * itemsPerPage + 1} a{" "} + {Math.min(currentPage * itemsPerPage, expensesData.total_registros)} de{" "} + {expensesData.total_registros} registros +

+ + + + { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }} + className={ + currentPage === 1 + ? "pointer-events-none opacity-50" + : "cursor-pointer" + } + /> + + {Array.from({ length: expensesData.total_paginas }, (_, i) => i + 1).map((page) => ( + + setCurrentPage(page)} + isActive={currentPage === page} + className="cursor-pointer" + > + {page} + + + ))} + + { + if (currentPage < expensesData.total_paginas) { + setCurrentPage(currentPage + 1); + } + }} + className={ + currentPage === expensesData.total_paginas + ? "pointer-events-none opacity-50" + : "cursor-pointer" + } + /> + + + +
+ )} +
+
+
+ ); +} diff --git a/src/modules/intelligence-ia/pages/Index.tsx b/src/modules/intelligence-ia/pages/Index.tsx new file mode 100644 index 0000000..3b8b411 --- /dev/null +++ b/src/modules/intelligence-ia/pages/Index.tsx @@ -0,0 +1,7 @@ +import Dashboard from "./Dashboard"; + +const Index = () => { + return ; +}; + +export default Index; diff --git a/src/modules/intelligence-ia/pages/Integrations.tsx b/src/modules/intelligence-ia/pages/Integrations.tsx new file mode 100644 index 0000000..b1d8a68 --- /dev/null +++ b/src/modules/intelligence-ia/pages/Integrations.tsx @@ -0,0 +1,312 @@ +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 { userProfileService } from "@/services/userProfile"; +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([]); + const [asanaUsers, setAsanaUsers] = useState([]); + const [loadingAsanaWorkspaces, setLoadingAsanaWorkspaces] = useState(false); + const [loadingAsanaUsers, setLoadingAsanaUsers] = useState(false); + const [asanaToken, setAsanaToken] = useState(""); + const [asanaApiKey, setAsanaApiKey] = useState(""); + const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(""); + const [selectedUserId, setSelectedUserId] = useState(""); + const [loadingIntegration, setLoadingIntegration] = useState(true); + const [hasIntegration, setHasIntegration] = useState(false); + const [integrationId, setIntegrationId] = useState(""); + const [userId, setUserId] = useState(""); + 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) { + console.log("Integrations: Não foi possível obter email do usuário"); + setLoadingIntegration(false); + return; + } + + // Busca o perfil do usuário para obter o ID + const userProfile = await userProfileService.getUserProfile(userEmail); + + if (!userProfile.success || !userProfile.id) { + console.log("Integrations: Usuário não encontrado ou sem 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 ( +
+ {/* Header */} +
+

+ Integrações +

+

+ Configure as conexões do seu agente com serviços externos +

+
+ + {/* Integrations Grid */} +
+ + + + + +
+
+ ); +} diff --git a/src/modules/intelligence-ia/pages/MeuPerfil.tsx b/src/modules/intelligence-ia/pages/MeuPerfil.tsx new file mode 100644 index 0000000..b4af161 --- /dev/null +++ b/src/modules/intelligence-ia/pages/MeuPerfil.tsx @@ -0,0 +1,516 @@ +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 { userProfileService, UserProfile } from "@/services/userProfile"; +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(""); + + 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) { + console.log("MeuPerfil: Não há token no sessionStorage"); + window.location.replace(import.meta.env.VITE_BASE_URL_HGTX_CORE || 'https://core.hgtx.com.br'); + return; + } + + console.log("MeuPerfil: Token encontrado no sessionStorage"); + + // Tenta obter o email do usuário logado + let userEmail = GlobalFunctions.getUsuarioLogado().email; + console.log("MeuPerfil: Email obtido:", userEmail); + + // Se não tem email, tenta fazer refresh do token + if (!userEmail) { + console.log("MeuPerfil: Email vazio, tentando fazer refresh do token"); + 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; + console.log("MeuPerfil: Email após refresh:", userEmail); + } 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(); + console.log("MeuPerfil: Dados do usuário após tentativas:", usuarioData); + + // 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") { + console.log("MeuPerfil: Token completamente inválido (sem UID/EID), redirecionando"); + window.location.replace(import.meta.env.VITE_BASE_URL_HGTX_CORE || 'https://core.hgtx.com.br'); + return; + } + + // Se tem token mas não tem email, pode ser que o email esteja em outro campo + // ou o token precisa ser renovado. Tenta carregar o perfil mesmo assim. + console.log("MeuPerfil: Token existe mas email vazio, tentando carregar perfil mesmo assim"); + } + + // Se passou na verificação (ou tem token válido), carrega o perfil + console.log("MeuPerfil: Carregando perfil do usuário"); + 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) { + console.log("MeuPerfil: Email não encontrado no token, tentando Transfer Area"); + const transferEmail = GlobalFunctions.getTransferProperty( + TransferAreaProperties.UsuarioEmail + ); + if (transferEmail) { + userEmail = transferEmail as string; + console.log("MeuPerfil: Email do Transfer Area:", userEmail); + } + } + + // 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; + console.log("MeuPerfil: Email após refresh no loadUserProfile:", userEmail); + } 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 userProfileService.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) => { + const formatted = formatPhoneNumber(e.target.value); + setWhatsapp(formatted); + }; + + const handleCreateWhatsappChange = (e: React.ChangeEvent) => { + 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 userProfileService.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 userProfileService.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 ( +
+
+ +

Carregando perfil...

+
+
+ ); + } + + return ( +
+
+

Meu Perfil

+

+ Gerencie suas informações pessoais +

+
+ + {!userExists ? ( + + +
+ +
+

+ Você ainda não possui uma conta +

+

+ Crie sua conta para começar a usar o agente pessoal de IA +

+
+ +
+
+
+ ) : ( + + + + + Informações Pessoais + + + +
+ + setNomeCompleto(e.target.value)} + placeholder="Digite seu nome completo" + /> +
+ +
+ + +

+ O e-mail não pode ser alterado +

+
+ +
+ + +
+ +
+
+ +

+ Receba notificações sobre eventos e tarefas +

+
+ +
+ + +
+
+ )} + + {/* Modal de Criação de Conta */} + + + + Criar Conta + + Preencha os dados abaixo para criar sua conta no agente pessoal de IA + + +
+
+ + setCreateNome(e.target.value)} + placeholder="Digite seu nome completo" + /> +
+ +
+ + +

+ O e-mail é baseado na sua conta logada +

+
+ +
+ + +
+ +
+
+ +

+ Receba notificações sobre eventos e tarefas +

+
+ +
+
+ + + + +
+
+
+ ); +}; + +export default MeuPerfil; diff --git a/src/modules/intelligence-ia/pages/NotFound.tsx b/src/modules/intelligence-ia/pages/NotFound.tsx new file mode 100644 index 0000000..7bc2234 --- /dev/null +++ b/src/modules/intelligence-ia/pages/NotFound.tsx @@ -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 ( +
+
+

404

+

Oops! Page not found

+ + Return to Home + +
+
+ ); +}; + +export default NotFound; diff --git a/src/services/asana.ts b/src/services/asana.ts new file mode 100644 index 0000000..9973158 --- /dev/null +++ b/src/services/asana.ts @@ -0,0 +1,435 @@ +/** + * Serviço para integração com API do Asana + */ + +/** + * Interface para workspace do Asana (resposta da API) + */ +export interface AsanaWorkspaceResponse { + gid: string; + resource_type: string; + name: string; +} + +/** + * Interface para resposta da API de workspaces + */ +export interface AsanaWorkspacesResponse { + data: AsanaWorkspaceResponse[]; +} + +/** + * Interface para workspace formatado (usado no componente) + */ +export interface AsanaWorkspace { + id: string; + name: string; +} + +/** + * Interface para usuário do Asana (resposta da API) + */ +export interface AsanaUserResponse { + gid: string; + name: string; + resource_type: string; +} + +/** + * Interface para resposta da API de usuários + */ +export interface AsanaUsersResponse { + data: AsanaUserResponse[]; +} + +/** + * Interface para usuário formatado (usado no componente) + */ +export interface AsanaUser { + id: string; + name: string; +} + +/** + * Interface para resposta da API de integração do Asana + */ +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; +} + +/** + * Interface para request de criação/atualização de integração + */ +export interface AsanaIntegrationRequest { + user_id?: string; + api_key: string; + workspace_gid: string; + workspace_nome: string; + usuario_asana_gid: string; + usuario_asana_nome: string; +} + +/** + * Serviço para integração com Asana + */ +class AsanaService { + private readonly BASE_URL = 'https://app.asana.com/api/1.0'; + private readonly INTEGRATION_BASE_URL = 'https://prod-hgtx-intelligence-n8n.hgtx.com.br'; + private readonly INTEGRATION_WEBHOOK_ID = 'c898beff-84cb-44df-a69c-6eff27ccd7aa'; + + /** + * Lista os workspaces do Asana usando o token fornecido + * + * @param token - Token de acesso do Asana + * @returns Promise com a lista de workspaces formatados + */ + async getWorkspaces(token: string): Promise { + 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 = { + 'accept': 'application/json', + 'authorization': `Bearer ${token.trim()}`, + }; + + const response = await axios.get( + `${this.BASE_URL}/workspaces`, + { 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((workspace) => ({ + id: workspace.gid, + name: workspace.name, + })); + } catch (error: unknown) { + console.error('Erro ao buscar workspaces 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?.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', + }; + } + } + + /** + * Lista os usuários de um workspace do Asana + * + * @param token - Token de acesso do Asana + * @param workspaceId - ID do workspace (gid) + * @returns Promise com a lista de usuários formatados + */ + async getUsers(token: string, workspaceId: string): Promise { + 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 = { + 'accept': 'application/json', + 'authorization': `Bearer ${token.trim()}`, + }; + + const response = await axios.get( + `${this.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); + + // 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?.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', + }; + } + } + + /** + * Obtém os detalhes da integração do Asana do usuário + * + * @param userId - ID do usuário + * @returns Promise com os detalhes da integração ou null se não houver + */ + async getIntegration(userId: string): Promise { + 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'); + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + const headers: Record = { + '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}`; + } + + // Em desenvolvimento, usa proxy do Vite para evitar CORS + const baseUrl = import.meta.env.DEV + ? '/api/intelligence' + : this.INTEGRATION_BASE_URL; + + const response = await axios.get( + `${baseUrl}/webhook/${this.INTEGRATION_WEBHOOK_ID}/codex/agente-pessoal/integracoes/asana/${userId}`, + { headers } + ); + + // Se success é false, retorna null (não tem integração) + 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; + } + } + + // Para outros erros, retorna null silenciosamente (não quebra a aplicação) + return null; + } + } + + /** + * Cria uma nova integração do Asana + * + * @param request - Dados da integração a ser criada + * @returns Promise com a resposta da criação + */ + async createIntegration(request: AsanaIntegrationRequest): Promise { + 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 = { + '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}`; + } + + // Em desenvolvimento, usa proxy do Vite para evitar CORS + const baseUrl = import.meta.env.DEV + ? '/api/intelligence' + : this.INTEGRATION_BASE_URL; + + const response = await axios.post( + `${baseUrl}/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', + }; + } + } + + /** + * Atualiza uma integração existente do Asana + * + * @param integracaoId - ID da integração a ser atualizada + * @param request - Dados da integração a ser atualizada + * @returns Promise com a resposta da atualização + */ + async updateIntegration(integracaoId: string, request: Omit): Promise { + 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'); + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + const headers: Record = { + '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}`; + } + + // Em desenvolvimento, usa proxy do Vite para evitar CORS + const baseUrl = import.meta.env.DEV + ? '/api/intelligence' + : this.INTEGRATION_BASE_URL; + + const response = await axios.put( + `${baseUrl}/webhook/${this.INTEGRATION_WEBHOOK_ID}/codex/agente-pessoal/integracoes/asana/${integracaoId}`, + 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(); diff --git a/src/services/index.ts b/src/services/index.ts index b0420b1..f1f65a0 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -6,7 +6,28 @@ export { apiService } from './api'; export { transcriptionService } from './transcription'; export { audioGenerationService, VOICE_OPTIONS } from './audioGeneration'; export { imageGenerationService, IMAGE_SIZE_OPTIONS } from './imageGeneration'; +export { userProfileService } from './userProfile'; +export { asanaService } from './asana'; export type { TranscriptionResponse, TranscriptionRequest } from './transcription'; export type { AudioGenerationResponse, AudioGenerationRequest, VoiceType } from './audioGeneration'; export type { ImageGenerationResponse, ImageGenerationRequest, ImageSize } from './imageGeneration'; +export type { + UserProfile, + CreateUserRequest, + UpdateUserRequest, + FinancialIndicators, + ExpenseItem, + ExpensesResponse, + ExpensesFilters, + ExpenseCategory +} from './userProfile'; +export type { + AsanaWorkspace, + AsanaWorkspaceResponse, + AsanaWorkspacesResponse, + AsanaUser, + AsanaUserResponse, + AsanaUsersResponse, + AsanaIntegrationResponse +} from './asana'; export type { ApiResponse, UserConfig, ApiError } from './types'; diff --git a/src/services/userProfile.ts b/src/services/userProfile.ts new file mode 100644 index 0000000..6d4104e --- /dev/null +++ b/src/services/userProfile.ts @@ -0,0 +1,577 @@ +import { apiService } from './api'; +import { GlobalFunctions } from '@/GlobalFunctions'; + +/** + * Interfaces para o serviço de perfil do usuário + */ +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; +} + +/** + * Interface para indicadores financeiros + */ +export interface FinancialIndicators { + success: boolean; + id: string; + nome: string; + email: string; + total_despesas: string; + total_corporativo: string; + total_pessoal: string; + message?: string; +} + +/** + * Interface para despesa individual da API + */ +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; +} + +/** + * Interface para resposta da API de despesas + */ +export interface ExpensesResponse { + success: boolean; + total_registros: number; + total_paginas: number; + per_page: number; + pagina_atual: number; + data: ExpenseItem[]; +} + +/** + * Interface para filtros de busca de despesas + */ +export interface ExpensesFilters { + page?: number; + per_page?: number; + descricao?: string; + categoria_id?: number; + data_inicial?: string; + data_final?: string; +} + +/** + * Interface para categoria de despesas + */ +export interface ExpenseCategory { + id: number; + nome: string; + descricao: string; + criado_em: string; + atualizado_em: string; +} + +/** + * Serviço para gerenciamento de perfil do usuário + */ +class UserProfileService { + // Em desenvolvimento, usa proxy do Vite para evitar CORS + // Em produção, usa URL direta (requer que servidor permita PUT no CORS) + private readonly BASE_URL = import.meta.env.DEV + ? '/api/intelligence' + : 'https://prod-hgtx-intelligence-n8n.hgtx.com.br'; + private readonly WEBHOOK_ID = '2299acaf-70ee-47e3-a6fc-56dcc678d651'; + private readonly GET_USER_ENDPOINT = `${this.BASE_URL}/webhook/${this.WEBHOOK_ID}/codex/agente-pessoal/user`; + private readonly CREATE_USER_ENDPOINT = `${this.BASE_URL}/webhook/codex/agente-pessoal/user`; + private readonly UPDATE_USER_ENDPOINT = `${this.BASE_URL}/webhook/${this.WEBHOOK_ID}/codex/agente-pessoal/user`; + private readonly GET_FINANCIAL_INDICATORS_ENDPOINT = `${this.BASE_URL}/webhook/${this.WEBHOOK_ID}/codex/agente-pessoal/financas/indicadores`; + private readonly GET_EXPENSES_ENDPOINT = `${this.BASE_URL}/webhook/${this.WEBHOOK_ID}/codex/agente-pessoal/financas`; + private readonly GET_CATEGORIES_ENDPOINT = `${this.BASE_URL}/webhook/codex/agente-pessoal/categorias`; + + /** + * Obtém os detalhes do usuário pelo email + * + * @param userEmail - Email do usuário (opcional, usa do GlobalFunctions se não fornecido) + * @returns Promise com os dados do usuário ou erro se não existir + */ + async getUserProfile(userEmail?: string): Promise { + const email = userEmail || GlobalFunctions.getUsuarioLogado().email; + + if (!email) { + throw { + success: false, + message: 'Email do usuário não fornecido', + }; + } + + try { + // Usa axios diretamente pois a URL é absoluta e diferente do baseURL + const axios = (await import('axios')).default; + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + // Prepara headers de autenticação + const headers: Record = { + 'Content-Type': '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.get( + `${this.GET_USER_ENDPOINT}/${email}`, + { headers } + ); + + return response.data; + } catch (error: any) { + // Se o usuário não existir, retorna o erro da API + 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, + }; + } + } + + /** + * Cria um novo usuário + * + * @param request - Dados do usuário a ser criado + * @returns Promise com os dados do usuário criado + */ + async createUser(request: CreateUserRequest): Promise { + // Validações + 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', + }; + } + + // Remove formatação do WhatsApp (apenas números) + const whatsappNumbers = request.whatsapp.replace(/\D/g, ''); + + try { + const axios = (await import('axios')).default; + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + // Prepara headers de autenticação + // A API pode precisar apenas da apikey, não do token JWT + const headers: Record = { + 'Content-Type': 'application/json', + }; + + // Adiciona API key (obrigatória) + if (apiKey) { + headers['apikey'] = apiKey; + } else { + console.warn('VITE_API_KEY não configurada'); + } + + // Adiciona token JWT se disponível (pode ser necessário) + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + console.log('Criando usuário:', { + url: this.CREATE_USER_ENDPOINT, + hasApiKey: !!apiKey, + hasToken: !!token, + data: { + nome: request.nome.trim(), + email: request.email.trim(), + whatsapp: whatsappNumbers, + followup: request.followup, + }, + }); + + const response = await axios.post( + this.CREATE_USER_ENDPOINT, + { + nome: request.nome.trim(), + email: request.email.trim(), + whatsapp: whatsappNumbers, + followup: request.followup, + }, + { headers } + ); + + return response.data; + } catch (error: any) { + // Retorna o erro da API se existir + if (error.response?.data) { + throw error.response.data; + } + + throw { + success: false, + message: error.message || 'Erro ao criar usuário', + status: error.response?.status, + }; + } + } + + /** + * Atualiza os dados do usuário + * + * @param userId - ID do usuário + * @param request - Dados a serem atualizados + * @returns Promise com os dados atualizados do usuário + */ + async updateUser(userId: string, request: UpdateUserRequest): Promise { + // Validações + 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', + }; + } + + // Remove formatação do WhatsApp (apenas números) + const whatsappNumbers = request.whatsapp.replace(/\D/g, ''); + + try { + const axios = (await import('axios')).default; + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + // Prepara headers de autenticação + const headers: Record = { + 'Content-Type': 'application/json', + }; + + // Adiciona API key (obrigatória) + if (apiKey) { + headers['apikey'] = apiKey; + } + + // Adiciona token JWT se disponível + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + console.log('Atualizando usuário:', { + url: `${this.UPDATE_USER_ENDPOINT}/${userId}`, + method: 'PUT', + hasApiKey: !!apiKey, + hasToken: !!token, + isDev: import.meta.env.DEV, + data: { + nome: request.nome.trim(), + whatsapp: whatsappNumbers, + followup: request.followup, + }, + }); + + // Usa PUT conforme documentação da API + const response = await axios.put( + `${this.UPDATE_USER_ENDPOINT}/${userId}`, + { + nome: request.nome.trim(), + whatsapp: whatsappNumbers, + followup: request.followup, + }, + { headers } + ); + + return response.data; + } catch (error: any) { + // Retorna o erro da API se existir + if (error.response?.data) { + throw error.response.data; + } + + throw { + success: false, + message: error.message || 'Erro ao atualizar usuário', + status: error.response?.status, + }; + } + } + + /** + * Obtém os indicadores financeiros do usuário + * + * @param userEmail - Email do usuário (opcional, usa do GlobalFunctions se não fornecido) + * @returns Promise com os indicadores financeiros + */ + async getFinancialIndicators(userEmail?: string): Promise { + 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; + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + // Prepara headers de autenticação + const headers: Record = { + 'Content-Type': '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.get( + `${this.GET_FINANCIAL_INDICATORS_ENDPOINT}/${email}`, + { headers } + ); + + return response.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?: 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, + }; + } + } + + /** + * Obtém a lista de despesas do usuário com paginação e filtros + * + * @param userEmail - Email do usuário (opcional, usa do GlobalFunctions se não fornecido) + * @param filters - Filtros de busca (página, itens por página, descrição, categoria, datas) + * @returns Promise com a lista de despesas paginada + */ + async getExpenses(userEmail?: string, filters?: ExpensesFilters): Promise { + 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; + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + // Prepara headers de autenticação + const headers: Record = { + 'Content-Type': 'application/json', + }; + + // Adiciona API key (obrigatória) + if (apiKey) { + headers['apikey'] = apiKey; + } + + // Adiciona token JWT se disponível + 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 = `${this.GET_EXPENSES_ENDPOINT}/${email}${queryString ? `?${queryString}` : ''}`; + + const response = await axios.get(url, { headers }); + + // A API retorna um array com um único objeto + 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, + }; + } + } + + /** + * Obtém a lista de categorias de despesas + * + * @returns Promise com a lista de categorias + */ + async getCategories(): Promise { + try { + const axios = (await import('axios')).default; + + // Obtém o token JWT para autenticação + const token = await GlobalFunctions.getToken(); + const apiKey = import.meta.env.VITE_API_KEY || ''; + + // Prepara headers de autenticação + const headers: Record = { + 'Content-Type': '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.get(this.GET_CATEGORIES_ENDPOINT, { 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); + + // Retorna array vazio em caso de erro para não quebrar a aplicação + return []; + } + } +} + +// Exporta instância única (Singleton) +export const userProfileService = new UserProfileService(); diff --git a/tailwind.config.ts b/tailwind.config.ts index 254556f..0dfbe2d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -34,6 +34,14 @@ export default { DEFAULT: "hsl(var(--destructive))", 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: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", diff --git a/vite.config.ts b/vite.config.ts index da25c6d..58e4f07 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,14 @@ export default defineConfig(({ mode }) => ({ server: { host: "::", 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), resolve: {