Reverted to commit b8d0db98bb

This commit is contained in:
gpt-engineer-app[bot] 2025-05-19 18:42:59 +00:00
parent ba5d3c92a3
commit 90b3030ebb
32 changed files with 673 additions and 1645 deletions

8
package-lock.json generated
View File

@ -41,7 +41,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
@ -4196,9 +4196,9 @@
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": {
"type": "github",

View File

@ -44,7 +44,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",

View File

@ -1,3 +1,4 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Index from '@/pages/Index';
import Transacoes from '@/pages/Transacoes';
@ -10,7 +11,6 @@ import WhatsApp from '@/pages/WhatsApp';
import { Toaster } from '@/components/ui/toaster';
import './App.css';
import ProtectedRoute from '@/components/auth/ProtectedRoute';
import RodrigoAudio from './pages/RodrigoAudio';
function App() {
return (
@ -49,7 +49,6 @@ function App() {
} />
<Route path="/login" element={<Auth />} />
<Route path="/auth" element={<Auth />} />
<Route path="/rodrigo-audio" element={<RodrigoAudio />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Toaster />

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Sidebar, defaultItems } from '@/components/layout/Sidebar';
import Sidebar from '@/components/layout/Sidebar';
import Header from '@/components/layout/Header';
import { useToast } from "@/components/ui/use-toast";
import { Menu } from 'lucide-react';
@ -39,11 +39,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0 w-[250px]">
<Sidebar items={defaultItems} className="w-full border-none" />
<Sidebar className="w-full border-none" />
</SheetContent>
</Sheet>
) : (
<Sidebar items={defaultItems} />
<Sidebar />
)}
<div className="flex flex-col flex-1 overflow-hidden">
<Header />

View File

@ -1,74 +1,99 @@
import React from "react";
import {
LayoutDashboard,
Users,
Settings,
MessageSquare,
Headphones
} from "lucide-react";
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, Home, CalendarDays, DollarSign, PieChart, Flag, MessageCircle } from 'lucide-react';
import { MainNavItem } from "@/types";
import { siteConfig } from "@/config/site";
// Updated navItems with the new WhatsApp connection page
const navItems = [
{
name: 'Dashboard',
path: '/dashboard',
icon: <Home className="mr-2 h-5 w-5" />
},
{
name: 'Transações',
path: '/transacoes',
icon: <DollarSign className="mr-2 h-5 w-5" />
},
{
name: 'Categorias',
path: '/categorias',
icon: <PieChart className="mr-2 h-5 w-5" />
},
{
name: 'Metas',
path: '/metas',
icon: <Flag className="mr-2 h-5 w-5" />
},
{
name: 'Calendário',
path: '/calendario',
icon: <CalendarDays className="mr-2 h-5 w-5" />
},
{
name: 'Conectar WhatsApp',
path: '/whatsapp',
icon: <MessageCircle className="mr-2 h-5 w-5" />
},
];
interface SidebarProps {
items?: MainNavItem[];
className?: string;
}
export function Sidebar({ items, className = "" }: SidebarProps) {
const Sidebar = ({ className }: SidebarProps) => {
const location = useLocation();
const [collapsed, setCollapsed] = React.useState(false);
// Improved function to verify if the item is active based on exact path
const isItemActive = (path: string) => {
return location.pathname === path;
};
return (
<div className={`flex flex-col space-y-6 w-full ${className}`}>
<a href="/" className="flex items-center space-x-2">
<img
src="/logo.png"
alt="Logo"
className="h-8 w-8 rounded-md"
/>
<span className="font-bold">{siteConfig.name}</span>
</a>
<div className="flex flex-col space-y-1">
{items?.map((item) => (
item.href ? (
<a
key={item.title}
href={item.href}
className="flex items-center space-x-2 px-4 py-2 rounded-md hover:bg-secondary"
>
{item.icon}
<span>{item.title}</span>
</a>
) : null
))}
<div
className={cn(
"flex flex-col h-full bg-sidebar border-r border-border transition-all duration-300 ease-in-out",
collapsed ? "w-[60px]" : "w-[250px]",
className
)}
>
<div className="flex items-center justify-between p-4">
{!collapsed && (
<h1 className="font-bold text-xl text-primary">FinDash</h1>
)}
<Button
variant="ghost"
size="icon"
onClick={() => setCollapsed(!collapsed)}
className="ml-auto"
>
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
</Button>
</div>
<nav className="flex-1 py-4 space-y-1">
{navItems.map((item) => (
<Link
key={item.name}
to={item.path}
className={cn(
"flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors",
isItemActive(item.path)
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent/50",
collapsed ? "justify-center" : "justify-start"
)}
>
<span className={collapsed ? "mr-0" : "mr-2"}>{item.icon}</span>
{!collapsed && <span>{item.name}</span>}
</Link>
))}
</nav>
</div>
);
}
};
export const defaultItems: MainNavItem[] = [
{
title: "Dashboard",
href: "/",
icon: <LayoutDashboard className="h-5 w-5" />,
},
{
title: "Usuários",
href: "/usuarios",
icon: <Users className="h-5 w-5" />,
},
{
title: "WhatsApp",
href: "/whatsapp",
icon: <MessageSquare className="h-5 w-5" />,
},
{
title: "Configurações",
href: "/configuracoes",
icon: <Settings className="h-5 w-5" />,
},
{
title: "Áudios Rodrigo",
href: "/rodrigo-audio",
icon: <Headphones className="h-5 w-5" />
},
];
export default Sidebar;

View File

@ -1,45 +0,0 @@
import React from "react";
import UsersList from "./UsersList";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { UserRoundSearch } from "lucide-react";
interface UsersDialogProps {
trigger?: React.ReactNode;
}
export function UsersDialog({ trigger }: UsersDialogProps) {
return (
<Dialog>
<DialogTrigger asChild>
{trigger || (
<Button variant="outline" size="sm" className="gap-2">
<UserRoundSearch className="h-4 w-4" />
<span>Usuários</span>
</Button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-[700px] max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Gerenciamento de Usuários</DialogTitle>
<DialogDescription>
Visualize e gerencie os usuários cadastrados na plataforma
</DialogDescription>
</DialogHeader>
<div className="py-4">
<UsersList />
</div>
</DialogContent>
</Dialog>
);
}
export default UsersDialog;

View File

@ -1,143 +0,0 @@
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { UserInfo, getUsersRegisteredToday, getAllUsers } from '@/services/userService';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/hooks/use-toast";
const UsersList: React.FC = () => {
const [usersToday, setUsersToday] = useState<UserInfo[]>([]);
const [allUsers, setAllUsers] = useState<UserInfo[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>("today");
const { toast } = useToast();
const loadUsers = async (tab: string = activeTab) => {
setLoading(true);
try {
if (tab === "today") {
const users = await getUsersRegisteredToday();
setUsersToday(users);
} else {
const users = await getAllUsers();
setAllUsers(users);
}
} catch (error) {
console.error("Erro ao carregar usuários:", error);
toast({
title: "Erro",
description: "Falha ao carregar dados de usuários.",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
loadUsers();
}, []);
const handleTabChange = (value: string) => {
setActiveTab(value);
loadUsers(value);
};
const formatDate = (dateString: string) => {
try {
return format(new Date(dateString), "dd/MM/yyyy HH:mm", { locale: ptBR });
} catch (error) {
return "Data inválida";
}
};
const displayUsers = activeTab === "today" ? usersToday : allUsers;
return (
<Card className="w-full">
<CardHeader>
<CardTitle>Usuários Cadastrados</CardTitle>
<CardDescription>
{activeTab === "today"
? `${usersToday.length} usuários cadastrados hoje`
: `${allUsers.length} usuários cadastrados no total`}
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="today" onValueChange={handleTabChange}>
<TabsList className="mb-4">
<TabsTrigger value="today">Cadastrados Hoje</TabsTrigger>
<TabsTrigger value="all">Todos os Usuários</TabsTrigger>
</TabsList>
<TabsContent value="today" className="space-y-4">
{loading ? (
<div className="text-center py-4">Carregando...</div>
) : usersToday.length > 0 ? (
<div className="space-y-2">
{usersToday.map((user) => (
<div key={user.id} className="border p-4 rounded-md">
<div className="font-semibold">{user.nome}</div>
<div className="text-sm text-muted-foreground">Email: {user.email}</div>
{user.empresa && (
<div className="text-sm text-muted-foreground">Empresa: {user.empresa}</div>
)}
<div className="text-xs text-muted-foreground mt-1">
Cadastrado em: {formatDate(user.created_at)}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-4">Nenhum usuário cadastrado hoje.</div>
)}
</TabsContent>
<TabsContent value="all" className="space-y-4">
{loading ? (
<div className="text-center py-4">Carregando...</div>
) : allUsers.length > 0 ? (
<div className="space-y-2">
{allUsers.map((user) => (
<div key={user.id} className="border p-4 rounded-md">
<div className="font-semibold">{user.nome}</div>
<div className="text-sm text-muted-foreground">Email: {user.email}</div>
{user.empresa && (
<div className="text-sm text-muted-foreground">Empresa: {user.empresa}</div>
)}
<div className="text-xs text-muted-foreground mt-1">
Cadastrado em: {formatDate(user.created_at)}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-4">Nenhum usuário cadastrado.</div>
)}
</TabsContent>
</Tabs>
</CardContent>
<CardFooter className="flex justify-end">
<Button
variant="outline"
onClick={() => loadUsers(activeTab)}
disabled={loading}
>
{loading ? "Carregando..." : "Atualizar"}
</Button>
</CardFooter>
</Card>
);
};
export default UsersList;

View File

@ -8,20 +8,20 @@ import { RefreshCw } from 'lucide-react';
interface InstanceListProps {
instances: WhatsAppInstance[];
onViewQrCode: (instance: WhatsAppInstance) => void;
onDeleteInstance: (instanceId: string) => void; // Only requires instanceId
onRestartInstance: (instance: WhatsAppInstance) => Promise<void>;
onLogoutInstance: (instance: WhatsAppInstance) => Promise<void>;
onDelete: (instanceId: string) => void;
onRestart: (instance: WhatsAppInstance) => Promise<void>;
onLogout: (instance: WhatsAppInstance) => Promise<void>;
onSetPresence: (instance: WhatsAppInstance, presence: 'online' | 'offline') => Promise<void>;
onRefreshInstances?: () => Promise<void>;
isRefreshing?: boolean;
onRefreshInstances: () => Promise<void>;
isRefreshing: boolean;
}
const InstanceList = ({
instances,
onViewQrCode,
onDeleteInstance,
onRestartInstance,
onLogoutInstance,
onDelete,
onRestart,
onLogout,
onSetPresence,
onRefreshInstances,
isRefreshing
@ -43,17 +43,15 @@ const InstanceList = ({
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Instâncias Criadas</h2>
{onRefreshInstances && (
<Button
variant="outline"
size="sm"
onClick={onRefreshInstances}
disabled={isRefreshing}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Atualizando...' : 'Atualizar Lista'}
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={onRefreshInstances}
disabled={isRefreshing}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Atualizando...' : 'Atualizar Lista'}
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{instances.map((instance) => (
@ -61,9 +59,9 @@ const InstanceList = ({
key={instance.instanceId}
instance={instance}
onViewQrCode={onViewQrCode}
onDelete={onDeleteInstance}
onRestart={onRestartInstance}
onLogout={onLogoutInstance}
onDelete={onDelete}
onRestart={onRestart}
onLogout={onLogout}
onSetPresence={onSetPresence}
/>
))}

View File

@ -1,103 +0,0 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { useToast } from '@/hooks/use-toast';
import { Label } from '@/components/ui/label';
import { AudioMessage, storeAudioMessageForRodrigo } from '@/services/audioMessageService';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
interface MessageWebhookProps {
instance?: WhatsAppInstance;
}
const MessageWebhook: React.FC<MessageWebhookProps> = ({ instance }) => {
const { toast } = useToast();
const [webhookUrl, setWebhookUrl] = useState<string>('');
const handleSetWebhook = async () => {
if (!instance) {
toast({
title: "Erro",
description: "Selecione uma instância primeiro",
variant: "destructive",
});
return;
}
try {
// Here you would set the webhook URL for the Evolution API
// This is a placeholder for the actual implementation
toast({
title: "Webhook Configurado",
description: `Webhook configurado para ${instance.instanceName}`,
});
// Simulate receiving an audio message (for testing purposes)
const testAudioMessage: AudioMessage = {
sender_id: '120363420212322973@g.us',
sender_name: 'Teste',
instance_id: instance.instanceId,
audio_url: 'https://example.com/audio.mp3',
duration: 30,
metadata: {
test: true
}
};
await storeAudioMessageForRodrigo(testAudioMessage);
} catch (error) {
console.error('Error setting webhook:', error);
toast({
title: "Erro",
description: "Erro ao configurar webhook",
variant: "destructive",
});
}
};
return (
<Card>
<CardHeader>
<CardTitle>Configurar Webhook para Mensagens</CardTitle>
<CardDescription>
Configure um webhook para receber mensagens da Evolution API.
Os áudios enviados para o grupo do Rodrigo serão armazenados automaticamente.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="webhook-url">URL do Webhook</Label>
<Input
id="webhook-url"
placeholder="https://seu-servidor.com/webhook"
value={webhookUrl}
onChange={(e) => setWebhookUrl(e.target.value)}
/>
</div>
{instance ? (
<p className="text-sm text-muted-foreground">
Configurando para: <strong>{instance.instanceName}</strong>
</p>
) : (
<p className="text-sm text-yellow-600">
Selecione uma instância primeiro.
</p>
)}
</div>
</CardContent>
<CardFooter>
<Button
onClick={handleSetWebhook}
disabled={!instance || !webhookUrl}
>
Configurar Webhook
</Button>
</CardFooter>
</Card>
);
};
export default MessageWebhook;

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useState } from 'react';
import {
Dialog,
DialogContent,
@ -10,22 +10,21 @@ import {
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { RefreshCw } from 'lucide-react';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { fetchQrCode } from '@/services/whatsAppService';
import { useToast } from '@/hooks/use-toast';
interface QrCodeDialogProps {
open: boolean;
setOpen: (open: boolean) => void;
instanceName: string;
phoneNumber: string;
onStatusCheck?: () => void;
onOpenChange: (open: boolean) => void;
activeInstance: WhatsAppInstance | null;
onStatusCheck: () => void;
}
const QrCodeDialog = ({
open,
setOpen,
instanceName,
phoneNumber,
onOpenChange,
activeInstance,
onStatusCheck
}: QrCodeDialogProps) => {
const { toast } = useToast();
@ -33,30 +32,23 @@ const QrCodeDialog = ({
const [qrCodeData, setQrCodeData] = useState<string | null>(null);
const [qrError, setQrError] = useState<string | null>(null);
// Load QR code when dialog opens
useEffect(() => {
if (open && instanceName) {
handleRefreshQrCode();
}
}, [open, instanceName]);
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
onOpenChange(newOpen);
// After dialog closes, trigger status check to update connection state
if (!newOpen && onStatusCheck) {
if (!newOpen) {
onStatusCheck();
}
};
const handleRefreshQrCode = async () => {
if (!instanceName) return;
if (!activeInstance) return;
setLoadingQR(true);
setQrError(null);
try {
const data = await fetchQrCode(instanceName);
const data = await fetchQrCode(activeInstance.instanceName);
console.log('QR Code API response:', data);
// Using the "base64" field from the response as the QR code data
@ -83,7 +75,7 @@ const QrCodeDialog = ({
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Conectar WhatsApp - {instanceName}</DialogTitle>
<DialogTitle>Conectar WhatsApp - {activeInstance?.instanceName}</DialogTitle>
<DialogDescription>
Escaneie o QR Code com seu WhatsApp para finalizar a conexão
</DialogDescription>
@ -117,7 +109,7 @@ const QrCodeDialog = ({
)}
<div className="flex space-x-2">
{instanceName && (
{activeInstance && (
<Button
variant="outline"
onClick={handleRefreshQrCode}
@ -128,7 +120,7 @@ const QrCodeDialog = ({
Atualizar QR Code
</Button>
)}
<Button variant="ghost" onClick={() => setOpen(false)}>
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Fechar
</Button>
</div>

View File

@ -1,8 +0,0 @@
// Export all WhatsApp components for easier imports
export { default as CreateInstanceForm } from './CreateInstanceForm';
export { default as InstanceList } from './InstanceList';
export { default as InstanceStats } from './InstanceStats';
export { default as QrCodeDialog } from './QrCodeDialog';
export { default as InstanceCard } from './InstanceCard';
export { default as MessageWebhook } from './MessageWebhook';

View File

@ -1,9 +0,0 @@
export const siteConfig = {
name: "WhatsApp Dashboard",
description: "Gerenciamento de instâncias de WhatsApp",
mainNav: [],
links: {
github: "https://github.com/",
},
};

View File

@ -1,3 +1,186 @@
// Re-export from the refactored location
export { useWhatsAppActions } from './whatsapp/useWhatsAppActions';
import { useState } from 'react';
import { useToast } from '@/hooks/use-toast';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import {
restartInstance,
logoutInstance,
deleteInstance,
setInstancePresence,
fetchQrCode
} from '@/services/whatsAppService';
export const useWhatsAppActions = (
updateInstance: (instance: WhatsAppInstance) => void,
removeInstance: (instanceId: string) => void,
checkAllInstancesStatus: () => Promise<void>
) => {
const { toast } = useToast();
const [activeInstance, setActiveInstance] = useState<WhatsAppInstance | null>(null);
const [qrDialogOpen, setQrDialogOpen] = useState(false);
// Handler for quando uma instância é reiniciada
const handleRestartInstance = async (instance: WhatsAppInstance) => {
try {
console.log(`Attempting to restart instance ${instance.instanceName}`);
await restartInstance(instance.instanceName);
// Atualiza o estado da instância para "connecting"
const updatedInstance = {
...instance,
connectionState: 'connecting' as const
};
console.log(`Instance ${instance.instanceName} restart initiated, setting state to connecting`, updatedInstance);
updateInstance(updatedInstance);
toast({
title: "Sucesso",
description: `Instância ${instance.instanceName} reiniciada com sucesso`
});
// Verifica o status após um breve delay para dar tempo de atualizar
setTimeout(() => checkAllInstancesStatus(), 3000);
} catch (error) {
console.error(`Error restarting instance ${instance.instanceName}:`, error);
toast({
title: "Erro",
description: `Falha ao reiniciar a instância ${instance.instanceName}`,
variant: "destructive",
});
}
};
// Handler for quando uma instância é desconectada
const handleLogoutInstance = async (instance: WhatsAppInstance) => {
try {
console.log(`Attempting to logout instance ${instance.instanceName}`);
await logoutInstance(instance.instanceName);
// Atualiza o estado da instância para "closed"
const updatedInstance = {
...instance,
connectionState: 'closed' as const,
status: 'disconnected'
};
console.log(`Instance ${instance.instanceName} logout successful, updating instance state`, updatedInstance);
updateInstance(updatedInstance);
toast({
title: "Sucesso",
description: `Instância ${instance.instanceName} desconectada com sucesso`
});
} catch (error) {
console.error(`Error logging out instance ${instance.instanceName}:`, error);
toast({
title: "Erro",
description: `Falha ao desconectar a instância ${instance.instanceName}`,
variant: "destructive",
});
}
};
// Handler for quando a presença é alterada
const handleSetPresence = async (instance: WhatsAppInstance, presence: 'online' | 'offline') => {
try {
console.log(`Setting presence to ${presence} for instance ${instance.instanceName}`);
await setInstancePresence(instance.instanceName, presence);
const updatedInstance = {
...instance,
presence: presence
};
console.log(`Updated instance with new presence status`, updatedInstance);
updateInstance(updatedInstance);
toast({
title: "Sucesso",
description: `Instância ${instance.instanceName} agora está ${presence === 'online' ? 'Online' : 'Offline'}`
});
} catch (error) {
console.error(`Error setting presence to ${presence} for instance ${instance.instanceName}:`, error);
toast({
title: "Erro",
description: `Falha ao definir presença ${presence} para ${instance.instanceName}`,
variant: "destructive",
});
}
};
// Handler for when an instance is deleted
const handleDeleteInstance = async (instanceId: string, instanceName: string) => {
console.log(`Deleting instance with ID: ${instanceId}, name: ${instanceName}`);
try {
// Call API to delete instance
const response = await deleteInstance(instanceName);
console.log(`Deletion API response for instance ${instanceName}:`, response);
// Remove from local state
removeInstance(instanceId);
// If we're viewing QR code for this instance, close the dialog
if (activeInstance?.instanceId === instanceId) {
setQrDialogOpen(false);
setActiveInstance(null);
}
toast({
title: "Instância removida",
description: "A instância do WhatsApp foi removida com sucesso.",
});
} catch (error) {
console.error(`Error deleting instance with ID ${instanceId}:`, error);
// Even if the API call fails, we still want to remove the instance from local storage
// This handles the case when instances are in inconsistent state with the server
removeInstance(instanceId);
if (activeInstance?.instanceId === instanceId) {
setQrDialogOpen(false);
setActiveInstance(null);
}
toast({
title: "Aviso",
description: "Instância removida localmente, mas pode haver falha na comunicação com o servidor.",
variant: "destructive",
});
}
};
// Handler for when QR code dialog is requested
const handleViewQrCode = async (instance: WhatsAppInstance) => {
console.log(`Opening QR code dialog for instance: ${instance.instanceName}`);
setActiveInstance(instance);
setQrDialogOpen(true);
try {
const data = await fetchQrCode(instance.instanceName);
console.log('QR Code API response:', data);
// If the fetchQrCode call was successful, QrCodeDialog will handle showing the QR code
} catch (error) {
console.error("Error initiating QR code fetch:", error);
toast({
title: "Erro ao obter QR Code",
description: "Falha ao iniciar obtenção de QR Code. Tente novamente.",
variant: "destructive",
});
setQrDialogOpen(false);
}
};
return {
activeInstance,
qrDialogOpen,
setQrDialogOpen,
handleRestartInstance,
handleLogoutInstance,
handleSetPresence,
handleDeleteInstance,
handleViewQrCode
};
};

View File

@ -1,3 +1,249 @@
// Re-export from the refactored location
export { useWhatsAppInstances } from './whatsapp/useWhatsAppInstances';
import { useState, useEffect, useCallback } from 'react';
import { useToast } from '@/hooks/use-toast';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import {
saveInstancesToLocalStorage,
loadInstancesFromLocalStorage,
fetchAllInstances,
fetchConnectionState
} from '@/services/whatsAppService';
export const useWhatsAppInstances = () => {
const { toast } = useToast();
const [instances, setInstances] = useState<WhatsAppInstance[]>([]);
const [currentUserId, setCurrentUserId] = useState<string>('');
const [isRefreshing, setIsRefreshing] = useState(false);
// Get current user ID and load instances on component mount
useEffect(() => {
const userId = localStorage.getItem('userId') || '';
console.log("Current userId from localStorage:", userId);
setCurrentUserId(userId);
if (userId) {
const userInstances = loadInstancesFromLocalStorage(userId);
console.log('Loaded user instances:', userInstances);
setInstances(userInstances);
}
}, []); // Only run once on mount
// Save instances to localStorage whenever they change
useEffect(() => {
if (currentUserId && instances.length > 0) {
console.log('Saving instances to localStorage:', instances);
saveInstancesToLocalStorage(instances, currentUserId);
}
}, [instances, currentUserId]);
// Function to check connection status for all instances
const checkAllInstancesStatus = useCallback(async () => {
if (instances.length === 0) return;
console.log(`Checking connection status for ${instances.length} instances`);
try {
const updatedInstances = await Promise.all(
instances.map(async (instance) => {
try {
console.log(`Checking status for ${instance.instanceName}`);
const state = await fetchConnectionState(instance.instanceName);
console.log(`Status for ${instance.instanceName}: ${state}`);
// Only update connectionState if it's different
if (instance.connectionState !== state) {
// Ensure the state is one of the valid values or treat as 'closed'
let validState: 'open' | 'closed' | 'connecting';
if (state === 'open' || state === 'connecting') {
validState = state;
} else {
validState = 'closed';
}
return {
...instance,
connectionState: validState,
status: state === 'open' ? 'connected' :
state === 'connecting' ? 'connecting' : 'disconnected'
};
}
return instance;
} catch (error) {
console.error(`Error checking status for ${instance.instanceName}:`, error);
// If the instance doesn't exist on the server, mark it as closed
if (error instanceof Error && error.message.includes("does not exist")) {
return {
...instance,
connectionState: 'closed' as const,
status: 'disconnected'
};
}
// Keep the current state in case of other errors
return instance;
}
})
);
// Only update state if there are actual changes
const hasChanges = JSON.stringify(updatedInstances) !== JSON.stringify(instances);
if (hasChanges) {
console.log('Updated instances after status check:', updatedInstances);
setInstances(updatedInstances);
} else {
console.log('No changes in instance status');
}
} catch (error) {
console.error("Error checking instances status:", error);
}
}, [instances]);
// Handler para atualizar a lista de instâncias do servidor
const refreshInstances = async () => {
if (!currentUserId) {
toast({
title: "Erro de autenticação",
description: "Você precisa estar logado para atualizar as instâncias",
variant: "destructive",
});
return;
}
setIsRefreshing(true);
try {
console.log("Fetching instances from server...");
const response = await fetchAllInstances();
console.log("Fetched instances from server:", response);
if (response.instances && Array.isArray(response.instances)) {
// Mapeia as instâncias do servidor para o formato correto com userId
const serverInstances: WhatsAppInstance[] = response.instances
.map(serverInstance => {
// Ensure we convert server state to valid connectionState
let connectionState: 'open' | 'closed' | 'connecting';
const state = serverInstance.state || 'closed';
if (state === 'open' || state === 'connecting') {
connectionState = state;
} else {
connectionState = 'closed';
}
return {
instanceName: serverInstance.instanceName,
instanceId: serverInstance.instanceName, // Using instanceName as the ID for consistency
phoneNumber: serverInstance.number || 'Desconhecido',
userId: currentUserId,
connectionState: connectionState,
status: connectionState === 'open' ? 'connected' :
connectionState === 'connecting' ? 'connecting' : 'disconnected'
};
});
console.log("Mapped server instances:", serverInstances);
// Identifica instâncias novas que não existem localmente
const localInstanceIds = new Set(instances.map(i => i.instanceId));
const newServerInstances = serverInstances.filter(i => !localInstanceIds.has(i.instanceId));
// Atualiza instâncias existentes com dados do servidor
const updatedExistingInstances = instances.map(localInstance => {
const serverMatch = serverInstances.find(si => si.instanceId === localInstance.instanceId);
if (serverMatch) {
return {
...localInstance,
connectionState: serverMatch.connectionState,
status: serverMatch.status
};
}
return localInstance;
});
// Combina tudo
const allInstances = [...updatedExistingInstances, ...newServerInstances];
console.log("Combined instances after refresh:", allInstances);
setInstances(allInstances);
toast({
title: "Sucesso",
description: `${serverInstances.length} instâncias encontradas no servidor`,
});
} else {
toast({
title: "Aviso",
description: "Nenhuma instância encontrada no servidor",
});
}
} catch (error) {
console.error("Erro ao buscar instâncias:", error);
toast({
title: "Erro",
description: "Falha ao buscar instâncias do servidor",
variant: "destructive",
});
} finally {
setIsRefreshing(false);
// Verificar status após atualizar a lista
await checkAllInstancesStatus();
}
};
// Add a new instance
const addInstance = (newInstance: WhatsAppInstance) => {
console.log("New instance created, adding to instances list:", newInstance);
// Verifica se já existe uma instância com o mesmo ID
const existingIndex = instances.findIndex(inst => inst.instanceId === newInstance.instanceId);
if (existingIndex >= 0) {
// Atualiza a instância existente
console.log(`Instance with ID ${newInstance.instanceId} already exists, updating it`);
setInstances(prevInstances =>
prevInstances.map((instance, index) =>
index === existingIndex ? newInstance : instance
)
);
} else {
// Adiciona nova instância
setInstances(prevInstances => [...prevInstances, newInstance]);
}
};
// Remove an instance
const removeInstance = (instanceId: string) => {
console.log(`Removing instance with ID: ${instanceId}`);
setInstances(prevInstances => {
const filtered = prevInstances.filter(instance => instance.instanceId !== instanceId);
console.log("Updated instances after deletion:", filtered);
return filtered;
});
};
// Update an instance
const updateInstance = (updatedInstance: WhatsAppInstance) => {
console.log(`Updating instance: ${updatedInstance.instanceName}`, updatedInstance);
setInstances(prevInstances => {
const newInstances = prevInstances.map(instance =>
instance.instanceId === updatedInstance.instanceId
? updatedInstance
: instance
);
console.log("Updated instances:", newInstances);
return newInstances;
});
};
return {
instances,
isRefreshing,
currentUserId,
addInstance,
removeInstance,
updateInstance,
refreshInstances,
checkAllInstancesStatus
};
};

View File

@ -1,76 +0,0 @@
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { fetchConnectionState } from '@/services/whatsAppService';
/**
* Checks the connection status for a single WhatsApp instance
*/
export const checkInstanceStatus = async (instance: WhatsAppInstance): Promise<WhatsAppInstance> => {
try {
console.log(`Checking status for ${instance.instanceName}`);
const state = await fetchConnectionState(instance.instanceName);
console.log(`Status for ${instance.instanceName}: ${state}`);
// Only update connectionState if it's different
if (instance.connectionState !== state) {
// Ensure the state is one of the valid values or treat as 'closed'
let validState: 'open' | 'closed' | 'connecting';
if (state === 'open' || state === 'connecting') {
validState = state;
} else {
validState = 'closed';
}
return {
...instance,
connectionState: validState,
status: state === 'open' ? 'connected' :
state === 'connecting' ? 'connecting' : 'disconnected'
};
}
return instance;
} catch (error) {
console.error(`Error checking status for ${instance.instanceName}:`, error);
// If the instance doesn't exist on the server, mark it as closed
if (error instanceof Error && error.message.includes("does not exist")) {
return {
...instance,
connectionState: 'closed' as const,
status: 'disconnected'
};
}
// Keep the current state in case of other errors
return instance;
}
};
/**
* Maps server instances to the local WhatsAppInstance format
*/
export const mapServerInstancesToLocal = (
serverInstances: any[],
currentUserId: string
): WhatsAppInstance[] => {
return serverInstances.map(serverInstance => {
// Ensure we convert server state to valid connectionState
let connectionState: 'open' | 'closed' | 'connecting';
const state = serverInstance.state || 'closed';
if (state === 'open' || state === 'connecting') {
connectionState = state;
} else {
connectionState = 'closed';
}
return {
instanceName: serverInstance.instanceName,
instanceId: serverInstance.instanceName, // Using instanceName as the ID for consistency
phoneNumber: serverInstance.number || 'Desconhecido',
userId: currentUserId,
connectionState: connectionState,
status: connectionState === 'open' ? 'connected' :
connectionState === 'connecting' ? 'connecting' : 'disconnected'
};
});
};

View File

@ -1,31 +0,0 @@
import { WhatsAppInstance } from '@/types/whatsAppTypes';
/**
* Load WhatsApp instances from localStorage for a specific user
*/
export const loadInstancesFromLocalStorage = (userId: string): WhatsAppInstance[] => {
try {
const storedData = localStorage.getItem(`whatsAppInstances_${userId}`);
if (storedData) {
const parsedData = JSON.parse(storedData);
console.log(`Loaded ${parsedData.length} instances from localStorage for user ${userId}`);
return parsedData;
}
} catch (error) {
console.error('Error loading WhatsApp instances from localStorage:', error);
}
return [];
};
/**
* Save WhatsApp instances to localStorage for a specific user
*/
export const saveInstancesToLocalStorage = (instances: WhatsAppInstance[], userId: string): void => {
try {
console.log(`Saving ${instances.length} instances to localStorage for user ${userId}`);
localStorage.setItem(`whatsAppInstances_${userId}`, JSON.stringify(instances));
} catch (error) {
console.error('Error saving WhatsApp instances to localStorage:', error);
}
};

View File

@ -1,18 +0,0 @@
import { WhatsAppInstance } from '@/types/whatsAppTypes';
export interface WhatsAppInstancesState {
instances: WhatsAppInstance[];
currentUserId: string;
isRefreshing: boolean;
}
export interface WhatsAppInstancesActions {
addInstance: (newInstance: WhatsAppInstance) => void;
removeInstance: (instanceId: string) => void;
updateInstance: (updatedInstance: WhatsAppInstance) => void;
refreshInstances: () => Promise<void>;
checkAllInstancesStatus: () => Promise<void>;
}
export type WhatsAppInstancesHookReturn = WhatsAppInstancesState & WhatsAppInstancesActions;

View File

@ -1,82 +0,0 @@
import { useState } from 'react';
import { useToast } from '@/hooks/use-toast';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { restartInstance, logoutInstance } from '@/services/whatsAppService';
/**
* Hook for handling WhatsApp instance connection actions like restart and logout
*/
export const useInstanceConnection = (
updateInstance: (instance: WhatsAppInstance) => void,
checkAllInstancesStatus: () => Promise<void>
) => {
const { toast } = useToast();
// Handler for when an instance is restarted
const handleRestartInstance = async (instance: WhatsAppInstance) => {
try {
console.log(`Attempting to restart instance ${instance.instanceName}`);
await restartInstance(instance.instanceName);
// Update the instance state to "connecting"
const updatedInstance = {
...instance,
connectionState: 'connecting' as const
};
console.log(`Instance ${instance.instanceName} restart initiated, setting state to connecting`, updatedInstance);
updateInstance(updatedInstance);
toast({
title: "Sucesso",
description: `Instância ${instance.instanceName} reiniciada com sucesso`
});
// Check status after a brief delay to give time to update
setTimeout(() => checkAllInstancesStatus(), 3000);
} catch (error) {
console.error(`Error restarting instance ${instance.instanceName}:`, error);
toast({
title: "Erro",
description: `Falha ao reiniciar a instância ${instance.instanceName}`,
variant: "destructive",
});
}
};
// Handler for when an instance is disconnected
const handleLogoutInstance = async (instance: WhatsAppInstance) => {
try {
console.log(`Attempting to logout instance ${instance.instanceName}`);
await logoutInstance(instance.instanceName);
// Update the instance state to "closed"
const updatedInstance = {
...instance,
connectionState: 'closed' as const,
status: 'disconnected'
};
console.log(`Instance ${instance.instanceName} logout successful, updating instance state`, updatedInstance);
updateInstance(updatedInstance);
toast({
title: "Sucesso",
description: `Instância ${instance.instanceName} desconectada com sucesso`
});
} catch (error) {
console.error(`Error logging out instance ${instance.instanceName}:`, error);
toast({
title: "Erro",
description: `Falha ao desconectar a instância ${instance.instanceName}`,
variant: "destructive",
});
}
};
return {
handleRestartInstance,
handleLogoutInstance
};
};

View File

@ -1,47 +0,0 @@
import { useToast } from '@/hooks/use-toast';
import { deleteInstance } from '@/services/whatsAppService';
/**
* Hook for handling WhatsApp instance deletion
*/
export const useInstanceDeletion = (
removeInstance: (instanceId: string) => void
) => {
const { toast } = useToast();
// Handler for when an instance is deleted
const handleDeleteInstance = async (instanceId: string, instanceName: string) => {
console.log(`Deleting instance with ID: ${instanceId}, name: ${instanceName}`);
try {
// Call API to delete instance
const response = await deleteInstance(instanceName);
console.log(`Deletion API response for instance ${instanceName}:`, response);
// Remove from local state
removeInstance(instanceId);
toast({
title: "Instância removida",
description: "A instância do WhatsApp foi removida com sucesso.",
});
} catch (error) {
console.error(`Error deleting instance with ID ${instanceId}:`, error);
// Even if the API call fails, we still want to remove the instance from local storage
// This handles the case when instances are in inconsistent state with the server
removeInstance(instanceId);
toast({
title: "Aviso",
description: "Instância removida localmente, mas pode haver falha na comunicação com o servidor.",
variant: "destructive",
});
}
};
return {
handleDeleteInstance
};
};

View File

@ -1,45 +0,0 @@
import { useToast } from '@/hooks/use-toast';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { setInstancePresence } from '@/services/whatsAppService';
/**
* Hook for handling WhatsApp instance presence (online/offline)
*/
export const useInstancePresence = (
updateInstance: (instance: WhatsAppInstance) => void
) => {
const { toast } = useToast();
// Handler for when presence is changed
const handleSetPresence = async (instance: WhatsAppInstance, presence: 'online' | 'offline') => {
try {
console.log(`Setting presence to ${presence} for instance ${instance.instanceName}`);
await setInstancePresence(instance.instanceName, presence);
const updatedInstance = {
...instance,
presence: presence
};
console.log(`Updated instance with new presence status`, updatedInstance);
updateInstance(updatedInstance);
toast({
title: "Sucesso",
description: `Instância ${instance.instanceName} agora está ${presence === 'online' ? 'Online' : 'Offline'}`
});
} catch (error) {
console.error(`Error setting presence to ${presence} for instance ${instance.instanceName}:`, error);
toast({
title: "Erro",
description: `Falha ao definir presença ${presence} para ${instance.instanceName}`,
variant: "destructive",
});
}
};
return {
handleSetPresence
};
};

View File

@ -1,43 +0,0 @@
import { useState } from 'react';
import { useToast } from '@/hooks/use-toast';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { fetchQrCode } from '@/services/whatsAppService';
/**
* Hook for handling WhatsApp QR code dialog functionality
*/
export const useQrCodeDialog = () => {
const { toast } = useToast();
const [activeInstance, setActiveInstance] = useState<WhatsAppInstance | null>(null);
const [qrDialogOpen, setQrDialogOpen] = useState(false);
// Handler for when QR code dialog is requested
const handleViewQrCode = async (instance: WhatsAppInstance) => {
console.log(`Opening QR code dialog for instance: ${instance.instanceName}`);
setActiveInstance(instance);
setQrDialogOpen(true);
try {
const data = await fetchQrCode(instance.instanceName);
console.log('QR Code API response:', data);
// If the fetchQrCode call was successful, QrCodeDialog will handle showing the QR code
} catch (error) {
console.error("Error initiating QR code fetch:", error);
toast({
title: "Erro ao obter QR Code",
description: "Falha ao iniciar obtenção de QR Code. Tente novamente.",
variant: "destructive",
});
setQrDialogOpen(false);
}
};
return {
activeInstance,
qrDialogOpen,
setQrDialogOpen,
handleViewQrCode
};
};

View File

@ -1,40 +0,0 @@
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { useInstanceConnection } from './useInstanceConnection';
import { useInstancePresence } from './useInstancePresence';
import { useInstanceDeletion } from './useInstanceDeletion';
import { useQrCodeDialog } from './useQrCodeDialog';
/**
* Main hook that combines all WhatsApp instance actions
*/
export const useWhatsAppActions = (
updateInstance: (instance: WhatsAppInstance) => void,
removeInstance: (instanceId: string) => void,
checkAllInstancesStatus: () => Promise<void>
) => {
// Combine all the action hooks
const { handleRestartInstance, handleLogoutInstance } =
useInstanceConnection(updateInstance, checkAllInstancesStatus);
const { handleSetPresence } =
useInstancePresence(updateInstance);
const { handleDeleteInstance } =
useInstanceDeletion(removeInstance);
const { activeInstance, qrDialogOpen, setQrDialogOpen, handleViewQrCode } =
useQrCodeDialog();
return {
// Re-export all actions and state from the specialized hooks
activeInstance,
qrDialogOpen,
setQrDialogOpen,
handleRestartInstance,
handleLogoutInstance,
handleSetPresence,
handleDeleteInstance,
handleViewQrCode
};
};

View File

@ -1,196 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { useToast } from '@/hooks/use-toast';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { fetchAllInstances } from '@/services/whatsAppService';
import {
loadInstancesFromLocalStorage,
saveInstancesToLocalStorage
} from './instanceStorageUtils';
import {
checkInstanceStatus,
mapServerInstancesToLocal
} from './instanceStatusUtils';
import { WhatsAppInstancesHookReturn } from './types';
/**
* Hook for managing WhatsApp instances
*/
export const useWhatsAppInstances = (): WhatsAppInstancesHookReturn => {
const { toast } = useToast();
const [instances, setInstances] = useState<WhatsAppInstance[]>([]);
const [currentUserId, setCurrentUserId] = useState<string>('');
const [isRefreshing, setIsRefreshing] = useState(false);
// Get current user ID and load instances on component mount
useEffect(() => {
const userId = localStorage.getItem('userId') || '';
console.log("Current userId from localStorage:", userId);
setCurrentUserId(userId);
if (userId) {
const userInstances = loadInstancesFromLocalStorage(userId);
console.log('Loaded user instances:', userInstances);
setInstances(userInstances);
}
}, []);
// Save instances to localStorage whenever they change
useEffect(() => {
if (currentUserId && instances.length > 0) {
console.log('Saving instances to localStorage:', instances);
saveInstancesToLocalStorage(instances, currentUserId);
}
}, [instances, currentUserId]);
// Check connection status for all instances
const checkAllInstancesStatus = useCallback(async () => {
if (instances.length === 0) return;
console.log(`Checking connection status for ${instances.length} instances`);
try {
const updatedInstances = await Promise.all(
instances.map(async (instance) => checkInstanceStatus(instance))
);
// Only update state if there are actual changes
const hasChanges = JSON.stringify(updatedInstances) !== JSON.stringify(instances);
if (hasChanges) {
console.log('Updated instances after status check:', updatedInstances);
setInstances(updatedInstances);
} else {
console.log('No changes in instance status');
}
} catch (error) {
console.error("Error checking instances status:", error);
}
}, [instances]);
// Refresh instances from the server
const refreshInstances = async () => {
if (!currentUserId) {
toast({
title: "Erro de autenticação",
description: "Você precisa estar logado para atualizar as instâncias",
variant: "destructive",
});
return;
}
setIsRefreshing(true);
try {
console.log("Fetching instances from server...");
const response = await fetchAllInstances();
console.log("Fetched instances from server:", response);
if (response.instances && Array.isArray(response.instances)) {
// Map server instances to local format
const serverInstances = mapServerInstancesToLocal(response.instances, currentUserId);
console.log("Mapped server instances:", serverInstances);
// Find new instances that don't exist locally
const localInstanceIds = new Set(instances.map(i => i.instanceId));
const newServerInstances = serverInstances.filter(i => !localInstanceIds.has(i.instanceId));
// Update existing instances with server data
const updatedExistingInstances = instances.map(localInstance => {
const serverMatch = serverInstances.find(si => si.instanceId === localInstance.instanceId);
if (serverMatch) {
return {
...localInstance,
connectionState: serverMatch.connectionState,
status: serverMatch.status
};
}
return localInstance;
});
// Combine everything
const allInstances = [...updatedExistingInstances, ...newServerInstances];
console.log("Combined instances after refresh:", allInstances);
setInstances(allInstances);
toast({
title: "Sucesso",
description: `${serverInstances.length} instâncias encontradas no servidor`,
});
} else {
toast({
title: "Aviso",
description: "Nenhuma instância encontrada no servidor",
});
}
} catch (error) {
console.error("Erro ao buscar instâncias:", error);
toast({
title: "Erro",
description: "Falha ao buscar instâncias do servidor",
variant: "destructive",
});
} finally {
setIsRefreshing(false);
// Check status after updating the list
await checkAllInstancesStatus();
}
};
// Add a new instance
const addInstance = (newInstance: WhatsAppInstance) => {
console.log("New instance created, adding to instances list:", newInstance);
// Check if instance with same ID already exists
const existingIndex = instances.findIndex(inst => inst.instanceId === newInstance.instanceId);
if (existingIndex >= 0) {
// Update existing instance
console.log(`Instance with ID ${newInstance.instanceId} already exists, updating it`);
setInstances(prevInstances =>
prevInstances.map((instance, index) =>
index === existingIndex ? newInstance : instance
)
);
} else {
// Add new instance
setInstances(prevInstances => [...prevInstances, newInstance]);
}
};
// Remove an instance
const removeInstance = (instanceId: string) => {
console.log(`Removing instance with ID: ${instanceId}`);
setInstances(prevInstances => {
const filtered = prevInstances.filter(instance => instance.instanceId !== instanceId);
console.log("Updated instances after deletion:", filtered);
return filtered;
});
};
// Update an instance
const updateInstance = (updatedInstance: WhatsAppInstance) => {
console.log(`Updating instance: ${updatedInstance.instanceName}`, updatedInstance);
setInstances(prevInstances => {
const newInstances = prevInstances.map(instance =>
instance.instanceId === updatedInstance.instanceId
? updatedInstance
: instance
);
console.log("Updated instances:", newInstances);
return newInstances;
});
};
return {
instances,
isRefreshing,
currentUserId,
addInstance,
removeInstance,
updateInstance,
refreshInstances,
checkAllInstancesStatus
};
};

View File

@ -147,42 +147,6 @@ export type Database = {
}
Relationships: []
}
rodrigo_audio_messages: {
Row: {
audio_url: string | null
created_at: string
duration: number | null
id: string
instance_id: string
message_id: string | null
metadata: Json | null
sender_id: string
sender_name: string | null
}
Insert: {
audio_url?: string | null
created_at?: string
duration?: number | null
id?: string
instance_id: string
message_id?: string | null
metadata?: Json | null
sender_id: string
sender_name?: string | null
}
Update: {
audio_url?: string | null
created_at?: string
duration?: number | null
id?: string
instance_id?: string
message_id?: string | null
metadata?: Json | null
sender_id?: string
sender_name?: string | null
}
Relationships: []
}
transacoes: {
Row: {
categoria: string | null
@ -190,7 +154,6 @@ export type Database = {
detalhes: string | null
estabelecimento: string | null
id: number
login: string | null
quando: string | null
tipo: string | null
user: string | null
@ -202,7 +165,6 @@ export type Database = {
detalhes?: string | null
estabelecimento?: string | null
id?: number
login?: string | null
quando?: string | null
tipo?: string | null
user?: string | null
@ -214,7 +176,6 @@ export type Database = {
detalhes?: string | null
estabelecimento?: string | null
id?: number
login?: string | null
quando?: string | null
tipo?: string | null
user?: string | null

View File

@ -1,84 +0,0 @@
import React, { useEffect, useState } from 'react';
import Layout from '@/components/layout/Layout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { AudioMessage, getRodrigoAudioMessages } from '@/services/audioMessageService';
import { format } from 'date-fns';
const RodrigoAudio = () => {
const [audioMessages, setAudioMessages] = useState<AudioMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetchAudioMessages = async () => {
setIsLoading(true);
try {
const messages = await getRodrigoAudioMessages();
setAudioMessages(messages);
} catch (error) {
console.error('Error fetching Rodrigo audio messages:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchAudioMessages();
}, []);
return (
<Layout>
<div className="container mx-auto p-4">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Áudios do Rodrigo</h1>
<Button
onClick={fetchAudioMessages}
disabled={isLoading}
>
{isLoading ? 'Carregando...' : 'Atualizar'}
</Button>
</div>
{audioMessages.length === 0 ? (
<Card>
<CardContent className="py-6">
<div className="text-center text-muted-foreground">
<p>Nenhuma mensagem de áudio encontrada para Rodrigo.</p>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{audioMessages.map((message) => (
<Card key={message.id}>
<CardHeader>
<CardTitle className="text-lg">
{message.sender_name || 'Usuário Desconhecido'}
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
Data: {message.created_at ? format(new Date(message.created_at), 'dd/MM/yyyy HH:mm') : 'N/A'}
</p>
<p className="text-sm text-muted-foreground">
Duração: {message.duration ? `${message.duration}s` : 'N/A'}
</p>
{message.audio_url && (
<audio controls className="w-full">
<source src={message.audio_url} type="audio/mpeg" />
Seu navegador não suporta áudio
</audio>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
</Layout>
);
};
export default RodrigoAudio;

View File

@ -1,27 +1,25 @@
import React, { useState, useEffect } from 'react';
import { useEffect } from 'react';
import Layout from '@/components/layout/Layout';
import {
CreateInstanceForm,
InstanceList,
InstanceStats,
QrCodeDialog
} from '@/components/whatsapp';
import { Button } from '@/components/ui/button';
import UsersDialog from '@/components/usuarios/UsersDialog';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import CreateInstanceForm from '@/components/whatsapp/CreateInstanceForm';
import InstanceList from '@/components/whatsapp/InstanceList';
import InstanceStats from '@/components/whatsapp/InstanceStats';
import QrCodeDialog from '@/components/whatsapp/QrCodeDialog';
import { useWhatsAppInstances } from '@/hooks/useWhatsAppInstances';
import { useWhatsAppActions } from '@/hooks/useWhatsAppActions';
import { useToast } from '@/hooks/use-toast';
const WhatsApp = () => {
const { toast } = useToast();
const {
instances,
isRefreshing,
currentUserId,
isRefreshing,
addInstance,
removeInstance,
updateInstance,
refreshInstances,
checkAllInstancesStatus
refreshInstances,
checkAllInstancesStatus
} = useWhatsAppInstances();
const {
@ -35,85 +33,119 @@ const WhatsApp = () => {
handleViewQrCode
} = useWhatsAppActions(updateInstance, removeInstance, checkAllInstancesStatus);
// Poll for status updates every 30 seconds
// Set up periodic status checks
useEffect(() => {
let interval: ReturnType<typeof setInterval> | null = null;
console.log("Setting up periodic status checks, current instances:", instances.length);
const startPolling = () => {
interval = setInterval(() => {
console.log("Polling for WhatsApp instance status updates");
checkAllInstancesStatus();
}, 30000); // Check every 30 seconds
};
// Check status initially
if (instances.length > 0) {
checkAllInstancesStatus();
}
// Start polling
startPolling();
// Cleanup on unmount
// Set up interval for periodic checks (every 30 seconds)
const interval = setInterval(() => {
if (instances.length > 0) {
console.log("Running periodic status check");
checkAllInstancesStatus();
}
}, 30000); // 30 seconds
// Clean up interval when component unmounts
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [checkAllInstancesStatus]);
}, [instances.length, checkAllInstancesStatus]);
// Create a wrapper function that matches the expected signature
const handleDeleteInstanceWrapper = (instanceId: string) => {
const instance = instances.find(inst => inst.instanceId === instanceId);
if (instance) {
handleDeleteInstance(instanceId, instance.instanceName);
} else {
console.error(`Instance with ID ${instanceId} not found`);
// Run refresh instances on initial load to get server instances
useEffect(() => {
const initialLoad = async () => {
if (instances.length === 0) {
try {
await refreshInstances();
} catch (error) {
console.error("Error on initial instance refresh:", error);
}
}
};
initialLoad();
}, []);
// Handler for when a new instance is created
const handleInstanceCreated = async (newInstance: WhatsAppInstance) => {
console.log('New instance to be added:', newInstance);
addInstance(newInstance);
// If there's a QR code in the response, show it
if (newInstance.qrcode) {
handleViewQrCode(newInstance);
}
// Trigger a status check for all instances
try {
await checkAllInstancesStatus();
} catch (error) {
console.error("Error checking status after instance creation:", error);
toast({
title: "Aviso",
description: "Instância criada, mas não foi possível verificar o status. Tente atualizar a lista manualmente.",
variant: "default",
});
}
};
// Handler for when an instance is deleted
const handleDeleteInstanceWrapper = (instanceId: string) => {
console.log(`Instance deletion requested for ID: ${instanceId}`);
const instanceToDelete = instances.find(i => i.instanceId === instanceId);
if (instanceToDelete) {
handleDeleteInstance(instanceId, instanceToDelete.instanceName);
} else {
console.error(`Instance with ID ${instanceId} not found for deletion`);
toast({
title: "Erro",
description: "Instância não encontrada para exclusão",
variant: "destructive",
});
}
};
console.log('Current instances in WhatsApp component:', instances);
return (
<Layout>
<div className="container mx-auto p-4">
<div className="flex flex-col md:flex-row justify-between items-center mb-6">
<h1 className="text-2xl font-bold mb-4 md:mb-0">WhatsApp Instances</h1>
<div className="flex gap-2 items-center">
<UsersDialog />
<Button
onClick={refreshInstances}
disabled={isRefreshing}
variant="outline"
>
{isRefreshing ? 'Atualizando...' : 'Atualizar Instâncias'}
</Button>
</div>
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold tracking-tight">Conectar WhatsApp</h1>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<CreateInstanceForm onInstanceCreated={addInstance} />
<div className="mt-8">
<InstanceList
instances={instances}
onRestartInstance={handleRestartInstance}
onLogoutInstance={handleLogoutInstance}
onSetPresence={handleSetPresence}
onDeleteInstance={handleDeleteInstanceWrapper}
onViewQrCode={handleViewQrCode}
onRefreshInstances={refreshInstances}
isRefreshing={isRefreshing}
/>
</div>
</div>
<div>
<InstanceStats instances={instances} />
</div>
</div>
{activeInstance && (
<QrCodeDialog
open={qrDialogOpen}
setOpen={setQrDialogOpen}
instanceName={activeInstance.instanceName}
phoneNumber={activeInstance.phoneNumber}
onStatusCheck={checkAllInstancesStatus}
/>
)}
{/* Form to create a new instance */}
<CreateInstanceForm onInstanceCreated={handleInstanceCreated} />
{/* Stats component */}
<InstanceStats instances={instances} />
{/* List of created instances */}
<InstanceList
instances={instances}
onViewQrCode={handleViewQrCode}
onDelete={handleDeleteInstanceWrapper}
onRestart={handleRestartInstance}
onLogout={handleLogoutInstance}
onSetPresence={handleSetPresence}
onRefreshInstances={refreshInstances}
isRefreshing={isRefreshing}
/>
{/* QR Code Dialog */}
<QrCodeDialog
open={qrDialogOpen}
onOpenChange={setQrDialogOpen}
activeInstance={activeInstance}
onStatusCheck={checkAllInstancesStatus}
/>
</div>
</Layout>
);

View File

@ -1,104 +0,0 @@
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
// Define types for audio message data
export interface AudioMessage {
id?: string;
created_at?: string;
sender_id: string;
sender_name?: string;
message_id?: string;
audio_url?: string;
duration?: number;
instance_id: string;
metadata?: Record<string, any>;
}
// Constants for specific users
const RODRIGO_GROUP_ID = '120363420212322973@g.us';
const RODRIGO_EMAIL = 'rodrigobm10@gmail.com';
/**
* Stores an audio message from a specific sender to Rodrigo's database
*/
export const storeAudioMessageForRodrigo = async (audioMessage: AudioMessage): Promise<boolean> => {
try {
console.log(`Storing audio message for Rodrigo from sender ${audioMessage.sender_id}`);
// Ensure this is a message for Rodrigo's group
if (audioMessage.sender_id !== RODRIGO_GROUP_ID) {
console.log('Message not for Rodrigo group, skipping storage');
return false;
}
// Insert the audio message into Rodrigo's table
const { data, error } = await supabase
.from('rodrigo_audio_messages')
.insert([audioMessage]);
if (error) {
console.error('Error storing audio message for Rodrigo:', error);
return false;
}
console.log('Successfully stored audio message for Rodrigo', data);
return true;
} catch (error) {
console.error('Exception storing audio message for Rodrigo:', error);
return false;
}
};
/**
* Retrieves all audio messages stored for Rodrigo
*/
export const getRodrigoAudioMessages = async (): Promise<AudioMessage[]> => {
try {
const { data, error } = await supabase
.from('rodrigo_audio_messages')
.select('*')
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching Rodrigo audio messages:', error);
return [];
}
return data as AudioMessage[];
} catch (error) {
console.error('Exception fetching Rodrigo audio messages:', error);
return [];
}
};
/**
* Hook for managing audio messages for Rodrigo
*/
export const useRodrigoAudioMessages = () => {
const { toast } = useToast();
const handleNewAudioMessage = async (audioMessage: AudioMessage): Promise<boolean> => {
const success = await storeAudioMessageForRodrigo(audioMessage);
if (success) {
toast({
title: "Áudio gravado",
description: `Áudio de ${audioMessage.sender_name || 'usuário'} foi gravado para o Rodrigo.`,
});
return true;
} else {
toast({
title: "Erro",
description: "Não foi possível gravar o áudio para o Rodrigo.",
variant: "destructive",
});
return false;
}
};
return {
handleNewAudioMessage,
getRodrigoAudioMessages
};
};

View File

@ -1,62 +0,0 @@
import { supabase } from "@/integrations/supabase/client";
import { format } from "date-fns";
export interface UserInfo {
id: string;
nome: string;
email: string;
empresa: string | null;
created_at: string;
}
export const getUsersRegisteredToday = async (): Promise<UserInfo[]> => {
try {
console.log("Buscando usuários cadastrados hoje...");
// Obtém a data atual no formato YYYY-MM-DD
const today = format(new Date(), 'yyyy-MM-dd');
// Busca usuários cadastrados hoje
const { data, error } = await supabase
.from("usuarios")
.select("*")
.gte('created_at', `${today}T00:00:00`) // Data início (hoje 00:00:00)
.lte('created_at', `${today}T23:59:59`) // Data fim (hoje 23:59:59)
.order('created_at', { ascending: false });
if (error) {
console.error("Erro ao buscar usuários:", error);
throw error;
}
console.log(`Encontrados ${data?.length || 0} usuários cadastrados hoje:`, data);
return data as UserInfo[];
} catch (error) {
console.error("Erro ao buscar usuários cadastrados hoje:", error);
throw error;
}
};
export const getAllUsers = async (): Promise<UserInfo[]> => {
try {
console.log("Buscando todos os usuários...");
const { data, error } = await supabase
.from("usuarios")
.select("*")
.order('created_at', { ascending: false });
if (error) {
console.error("Erro ao buscar usuários:", error);
throw error;
}
console.log(`Encontrados ${data?.length || 0} usuários no total`);
return data as UserInfo[];
} catch (error) {
console.error("Erro ao buscar todos os usuários:", error);
throw error;
}
};

View File

@ -1,125 +0,0 @@
import { AudioMessage, storeAudioMessageForRodrigo } from './audioMessageService';
// Constants for specific users
const RODRIGO_GROUP_ID = '120363420212322973@g.us';
const RODRIGO_EMAIL = 'rodrigobm10@gmail.com';
interface WebhookMessageEvent {
instance: {
id: string;
name: string;
};
message: {
id: string;
from: string;
fromMe: boolean;
to: string;
body: string;
type: string;
caption?: string;
timestamp: number;
hasMedia: boolean;
mediaUrl?: string;
mediaMimeType?: string;
mediaDuration?: number;
sender?: {
id: string;
name?: string;
pushname?: string;
};
};
}
/**
* Process an incoming webhook event from the Evolution API
*/
export const processWebhookEvent = async (event: any): Promise<boolean> => {
try {
console.log('Processing webhook event:', JSON.stringify(event));
// Ensure this is a message event
if (!event.message) {
console.log('Not a message event, skipping');
return false;
}
const messageEvent = event as WebhookMessageEvent;
// Check if this is an audio message
if (messageEvent.message.type === 'audio' || messageEvent.message.type === 'voice' || messageEvent.message.type === 'ptt') {
return await processAudioMessage(messageEvent);
}
return false;
} catch (error) {
console.error('Error processing webhook event:', error);
return false;
}
};
/**
* Process an audio message from the webhook event
*/
const processAudioMessage = async (event: WebhookMessageEvent): Promise<boolean> => {
// Check if the message is for Rodrigo's group
if (event.message.to === RODRIGO_GROUP_ID || event.message.from === RODRIGO_GROUP_ID) {
console.log(`Audio message for Rodrigo detected from ${event.message.from}`);
const audioMessage: AudioMessage = {
sender_id: event.message.from,
sender_name: event.message.sender?.name || event.message.sender?.pushname,
message_id: event.message.id,
audio_url: event.message.mediaUrl,
duration: event.message.mediaDuration,
instance_id: event.instance.id,
metadata: {
timestamp: event.message.timestamp,
mediaType: event.message.mediaMimeType,
caption: event.message.caption
}
};
return await storeAudioMessageForRodrigo(audioMessage);
}
return false;
};
/**
* Configure a webhook for an instance in the Evolution API
*/
export const configureWebhook = async (
instanceName: string,
webhookUrl: string,
apiKey: string
): Promise<boolean> => {
try {
const serverUrl = "evolutionapi2.innova1001.com.br";
const response = await fetch(`https://${serverUrl}/instance/webhook/${encodeURIComponent(instanceName)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'apikey': apiKey
},
body: JSON.stringify({
url: webhookUrl,
events: ['messages', 'messages.ack', 'messages.upsert']
})
});
if (!response.ok) {
const errorData = await response.json();
console.error('Error configuring webhook:', errorData);
return false;
}
const data = await response.json();
console.log('Webhook configuration response:', data);
return true;
} catch (error) {
console.error('Error configuring webhook:', error);
return false;
}
};

View File

@ -1,10 +0,0 @@
import { ReactNode } from "react";
export interface MainNavItem {
title: string;
href?: string;
icon?: ReactNode;
disabled?: boolean;
external?: boolean;
}

View File

@ -1,6 +1 @@
project_id = "tnurlgbvfsxwqgwxamni"
# Enable webhook edge function without JWT verification
[functions.whatsapp-webhook]
verify_jwt = false
project_id = "tnurlgbvfsxwqgwxamni"

View File

@ -1,132 +0,0 @@
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4";
// Constants for specific users
const RODRIGO_GROUP_ID = '120363420212322973@g.us';
const RODRIGO_EMAIL = 'rodrigobm10@gmail.com';
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
};
interface AudioMessage {
sender_id: string;
sender_name?: string;
message_id?: string;
audio_url?: string;
duration?: number;
instance_id: string;
metadata?: Record<string, any>;
}
const supabaseUrl = Deno.env.get('SUPABASE_URL') || '';
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || '';
const supabase = createClient(supabaseUrl, supabaseServiceKey);
async function storeAudioMessageForRodrigo(audioMessage: AudioMessage) {
try {
// Insert the audio message into Rodrigo's table
const { data, error } = await supabase
.from('rodrigo_audio_messages')
.insert([audioMessage]);
if (error) {
console.error('Error storing audio message for Rodrigo:', error);
return false;
}
console.log('Successfully stored audio message for Rodrigo', data);
return true;
} catch (error) {
console.error('Exception storing audio message for Rodrigo:', error);
return false;
}
}
// Process the webhook event and store audio messages for Rodrigo
async function processWebhookEvent(event: any): Promise<boolean> {
try {
console.log('Processing webhook event:', JSON.stringify(event));
// Ensure this is a message event
if (!event.message) {
console.log('Not a message event, skipping');
return false;
}
// Check if this is an audio message
const messageType = event.message.type;
if (messageType === 'audio' || messageType === 'voice' || messageType === 'ptt') {
// Check if the message is for Rodrigo's group
if (event.message.to === RODRIGO_GROUP_ID || event.message.from === RODRIGO_GROUP_ID) {
console.log(`Audio message for Rodrigo detected from ${event.message.from}`);
const audioMessage: AudioMessage = {
sender_id: event.message.from,
sender_name: event.message.sender?.name || event.message.sender?.pushname,
message_id: event.message.id,
audio_url: event.message.mediaUrl,
duration: event.message.mediaDuration,
instance_id: event.instance.id,
metadata: {
timestamp: event.message.timestamp,
mediaType: event.message.mediaMimeType,
caption: event.message.caption
}
};
return await storeAudioMessageForRodrigo(audioMessage);
}
}
return false;
} catch (error) {
console.error('Error processing webhook event:', error);
return false;
}
}
serve(async (req) => {
// Handle CORS preflight requests
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
if (req.method === "POST") {
const event = await req.json();
// Process the webhook event
const success = await processWebhookEvent(event);
return new Response(
JSON.stringify({ success }),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 200,
}
);
}
return new Response(
JSON.stringify({ error: "Method not allowed" }),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 405,
}
);
} catch (error) {
console.error("Error handling webhook:", error);
return new Response(
JSON.stringify({ error: "Internal server error" }),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 500,
}
);
}
});