Add advanced instance management features

Implement instance listing, restart, disconnect, delete, and presence setting functionalities with confirmation dialogs and UI updates.
This commit is contained in:
gpt-engineer-app[bot] 2025-05-19 15:07:24 +00:00
parent 686b682c19
commit b0d6e88a76
5 changed files with 593 additions and 63 deletions

View File

@ -1,63 +1,261 @@
import { useState } from 'react';
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { QrCode, Smartphone } from 'lucide-react';
import {
QrCode,
Smartphone,
RefreshCw,
X,
PowerOff,
CircleDot,
CircleOff
} from 'lucide-react';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
interface InstanceCardProps {
instance: WhatsAppInstance;
onViewQrCode: (instance: WhatsAppInstance) => void;
onDelete: (instanceId: string) => void;
onRestart: (instance: WhatsAppInstance) => Promise<void>;
onLogout: (instance: WhatsAppInstance) => Promise<void>;
onSetPresence: (instance: WhatsAppInstance, presence: 'online' | 'offline') => Promise<void>;
}
const InstanceCard = ({ instance, onViewQrCode, onDelete }: InstanceCardProps) => {
const InstanceCard = ({
instance,
onViewQrCode,
onDelete,
onRestart,
onLogout,
onSetPresence
}: InstanceCardProps) => {
const [loading, setLoading] = useState<string | null>(null);
const [confirmAction, setConfirmAction] = useState<{
open: boolean;
title: string;
description: string;
action: () => Promise<void>;
actionLabel: string;
} | null>(null);
// Função para processar uma ação com confirmação
const handleConfirmAction = (
title: string,
description: string,
action: () => Promise<void>,
actionLabel: string = "Confirmar"
) => {
setConfirmAction({
open: true,
title,
description,
action,
actionLabel
});
};
// Executa a ação após confirmação
const executeAction = async () => {
if (!confirmAction) return;
try {
setLoading(confirmAction.title);
await confirmAction.action();
} catch (error) {
console.error("Error executing action:", error);
} finally {
setLoading(null);
setConfirmAction(null);
}
};
// Handler para reiniciar instância
const handleRestart = () => {
handleConfirmAction(
"Reiniciar Instância",
`Deseja realmente reiniciar a instância "${instance.instanceName}"? A conexão atual será fechada e restabelecida.`,
async () => {
await onRestart(instance);
},
"Reiniciar"
);
};
// Handler para deslogar instância
const handleLogout = () => {
handleConfirmAction(
"Desconectar Instância",
`Deseja realmente desconectar a instância "${instance.instanceName}"? Você precisará escanear o QR Code novamente para reconectar.`,
async () => {
await onLogout(instance);
},
"Desconectar"
);
};
// Handler para apagar instância
const handleDelete = () => {
handleConfirmAction(
"Excluir Instância",
`Deseja realmente excluir a instância "${instance.instanceName}"? Esta ação não pode ser desfeita.`,
async () => {
onDelete(instance.instanceId);
},
"Excluir"
);
};
// Handler para definir presença online
const handleSetOnline = () => {
handleConfirmAction(
"Definir Presença Online",
`Deseja alterar o status da instância "${instance.instanceName}" para Online?`,
async () => {
await onSetPresence(instance, 'online');
},
"Definir Online"
);
};
// Handler para definir presença offline
const handleSetOffline = () => {
handleConfirmAction(
"Definir Presença Offline",
`Deseja alterar o status da instância "${instance.instanceName}" para Offline?`,
async () => {
await onSetPresence(instance, 'offline');
},
"Definir Offline"
);
};
return (
<Card className="h-full">
<CardHeader className="pb-2">
<CardTitle className="text-lg">{instance.instanceName}</CardTitle>
</CardHeader>
<CardContent className="pb-2">
<div className="space-y-2">
<div className="flex items-center text-sm">
<Smartphone className="h-4 w-4 mr-2 text-gray-500" />
<span>{instance.phoneNumber}</span>
<>
<Card className="h-full">
<CardHeader className="pb-2">
<CardTitle className="text-lg">{instance.instanceName}</CardTitle>
</CardHeader>
<CardContent className="pb-2">
<div className="space-y-2">
<div className="flex items-center text-sm">
<Smartphone className="h-4 w-4 mr-2 text-gray-500" />
<span>{instance.phoneNumber}</span>
</div>
<div className="flex items-center text-sm">
{instance.connectionState === 'open' ? (
<Badge variant="outline" className="flex items-center gap-1 text-green-500 border-green-300">
<span className="inline-block w-2 h-2 rounded-full bg-green-500"></span>
Status: Conectado
</Badge>
) : (
<Badge variant="outline" className="flex items-center gap-1 text-red-500 border-red-300">
<span className="inline-block w-2 h-2 rounded-full bg-red-500"></span>
Status: Desconectado
</Badge>
)}
</div>
</div>
<div className="flex items-center text-sm">
{instance.connectionState === 'open' ? (
<Badge variant="success" className="flex items-center gap-1">
<span className="inline-block w-2 h-2 rounded-full bg-green-500"></span>
Status: Conectado
</Badge>
) : (
<Badge variant="outline" className="flex items-center gap-1 text-red-500 border-red-300">
<span className="inline-block w-2 h-2 rounded-full bg-red-500"></span>
Status: Desconectado
</Badge>
)}
</CardContent>
<CardFooter className="flex flex-col gap-2">
<div className="flex justify-between w-full">
<Button
variant="outline"
size="sm"
onClick={() => onViewQrCode(instance)}
className="flex items-center"
>
<QrCode className="h-4 w-4 mr-2" />
Ver QR Code
</Button>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={handleDelete}
disabled={loading !== null}
>
<X className="h-4 w-4 mr-2" />
Excluir
</Button>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button
variant="outline"
size="sm"
onClick={() => onViewQrCode(instance)}
className="flex items-center"
>
<QrCode className="h-4 w-4 mr-2" />
Ver QR Code
</Button>
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={() => onDelete(instance.instanceId)}
>
Remover
</Button>
</CardFooter>
</Card>
<div className="grid grid-cols-2 gap-2 w-full">
<Button
variant="outline"
size="sm"
onClick={handleRestart}
disabled={loading !== null}
>
<RefreshCw className="h-4 w-4 mr-1" />
Reiniciar
</Button>
<Button
variant="outline"
size="sm"
onClick={handleLogout}
disabled={loading !== null}
>
<PowerOff className="h-4 w-4 mr-1" />
Desconectar
</Button>
</div>
<div className="grid grid-cols-2 gap-2 w-full">
<Button
variant="outline"
size="sm"
onClick={handleSetOnline}
disabled={loading !== null}
className="bg-green-50 hover:bg-green-100 text-green-700"
>
<CircleDot className="h-4 w-4 mr-1" />
Definir Online
</Button>
<Button
variant="outline"
size="sm"
onClick={handleSetOffline}
disabled={loading !== null}
className="bg-red-50 hover:bg-red-100 text-red-700"
>
<CircleOff className="h-4 w-4 mr-1" />
Definir Offline
</Button>
</div>
</CardFooter>
</Card>
<AlertDialog open={confirmAction?.open} onOpenChange={(open) => !open && setConfirmAction(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{confirmAction?.title}</AlertDialogTitle>
<AlertDialogDescription>
{confirmAction?.description}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading !== null}>Cancelar</AlertDialogCancel>
<AlertDialogAction
onClick={executeAction}
disabled={loading !== null}
className={loading === confirmAction?.title ? "opacity-50 cursor-not-allowed" : ""}
>
{loading === confirmAction?.title ? "Processando..." : confirmAction?.actionLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};

View File

@ -1,15 +1,31 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import InstanceCard from './InstanceCard';
import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { RefreshCw } from 'lucide-react';
interface InstanceListProps {
instances: WhatsAppInstance[];
onViewQrCode: (instance: WhatsAppInstance) => 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;
}
const InstanceList = ({ instances, onViewQrCode, onDelete }: InstanceListProps) => {
const InstanceList = ({
instances,
onViewQrCode,
onDelete,
onRestart,
onLogout,
onSetPresence,
onRefreshInstances,
isRefreshing
}: InstanceListProps) => {
if (instances.length === 0) {
return (
<Card>
@ -25,7 +41,18 @@ const InstanceList = ({ instances, onViewQrCode, onDelete }: InstanceListProps)
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Instâncias Criadas</h2>
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Instâncias Criadas</h2>
<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) => (
<InstanceCard
@ -33,6 +60,9 @@ const InstanceList = ({ instances, onViewQrCode, onDelete }: InstanceListProps)
instance={instance}
onViewQrCode={onViewQrCode}
onDelete={onDelete}
onRestart={onRestart}
onLogout={onLogout}
onSetPresence={onSetPresence}
/>
))}
</div>

View File

@ -7,7 +7,12 @@ import {
fetchQrCode,
fetchConnectionState,
saveInstancesToLocalStorage,
loadInstancesFromLocalStorage
loadInstancesFromLocalStorage,
fetchAllInstances,
restartInstance,
logoutInstance,
deleteInstance,
setInstancePresence
} from '@/services/whatsAppService';
import CreateInstanceForm from '@/components/whatsapp/CreateInstanceForm';
import InstanceList from '@/components/whatsapp/InstanceList';
@ -20,6 +25,7 @@ const WhatsApp = () => {
const [qrDialogOpen, setQrDialogOpen] = useState(false);
const [statusCheckInterval, setStatusCheckInterval] = useState<NodeJS.Timeout | null>(null);
const [currentUserId, setCurrentUserId] = useState<string>('');
const [isRefreshing, setIsRefreshing] = useState(false);
// Get current user ID and load instances on component mount
useEffect(() => {
@ -113,6 +119,155 @@ const WhatsApp = () => {
}
};
// Handler para atualizar a lista de instâncias do servidor
const handleRefreshInstances = 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 {
const response = await fetchAllInstances();
console.log("Fetched instances from server:", response);
if (response.instances && Array.isArray(response.instances)) {
// Filtra instâncias para mostrar apenas as do usuário atual
// E mapeia para o formato correto com userId
const serverInstances: WhatsAppInstance[] = response.instances
.filter(serverInstance => {
// Implemente sua lógica de filtro para o usuário atual (se necessário)
// Por padrão, vamos assumir que todas as instâncias são do usuário atual
return true;
})
.map(serverInstance => ({
instanceName: serverInstance.instanceName,
instanceId: serverInstance.instanceName,
phoneNumber: serverInstance.number || 'Desconhecido',
userId: currentUserId,
connectionState: serverInstance.state || 'closed',
status: serverInstance.status || 'unknown'
}));
// Mesclando instâncias do servidor com as locais (para não perder dados locais)
const localInstanceIds = new Set(instances.map(i => i.instanceId));
const newInstances = [
...instances,
...serverInstances.filter(i => !localInstanceIds.has(i.instanceId))
];
setInstances(newInstances);
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
checkAllInstancesStatus();
}
};
// Handler for quando uma instância é reiniciada
const handleRestartInstance = async (instance: WhatsAppInstance) => {
try {
await restartInstance(instance.instanceName);
// Atualiza o estado da instância para "connecting"
setInstances(prev =>
prev.map(i =>
i.instanceId === instance.instanceId
? { ...i, connectionState: 'connecting' as const }
: i
)
);
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 {
await logoutInstance(instance.instanceName);
// Atualiza o estado da instância para "closed"
setInstances(prev =>
prev.map(i =>
i.instanceId === instance.instanceId
? { ...i, connectionState: 'closed' as const }
: i
)
);
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 {
await setInstancePresence(instance.instanceName, presence);
toast({
title: "Sucesso",
description: `Instância ${instance.instanceName} agora está ${presence === 'online' ? 'Online' : 'Offline'}`
});
// Não precisamos alterar o estado da instância aqui, pois isso não afeta o connectionState
} 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 a new instance is created
const handleInstanceCreated = async (newInstance: WhatsAppInstance) => {
console.log("New instance created, adding to instances list:", newInstance);
@ -152,23 +307,49 @@ const WhatsApp = () => {
};
// Handler for when an instance is deleted
const handleDeleteInstance = (instanceId: string) => {
const handleDeleteInstance = async (instanceId: string) => {
console.log(`Deleting instance with ID: ${instanceId}`);
setInstances(prevInstances => {
const filtered = prevInstances.filter(instance => instance.instanceId !== instanceId);
console.log("Updated instances after deletion:", filtered);
return filtered;
});
// If we're viewing QR code for this instance, close the dialog
if (activeInstance?.instanceId === instanceId) {
setQrDialogOpen(false);
// Find the instance before deleting it
const instanceToDelete = instances.find(i => i.instanceId === instanceId);
if (!instanceToDelete) {
toast({
title: "Erro",
description: "Instância não encontrada",
variant: "destructive",
});
return;
}
toast({
title: "Instância removida",
description: "A instância do WhatsApp foi removida com sucesso.",
});
try {
// Call API to delete instance
await deleteInstance(instanceToDelete.instanceName);
// Remove from local state if API call was successful
setInstances(prevInstances => {
const filtered = prevInstances.filter(instance => instance.instanceId !== instanceId);
console.log("Updated instances after deletion:", filtered);
return filtered;
});
// If we're viewing QR code for this instance, close the dialog
if (activeInstance?.instanceId === instanceId) {
setQrDialogOpen(false);
}
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);
toast({
title: "Erro",
description: "Falha ao excluir a instância. Tente novamente.",
variant: "destructive",
});
}
};
return (
@ -185,7 +366,12 @@ const WhatsApp = () => {
<InstanceList
instances={instances}
onViewQrCode={handleViewQrCode}
onDelete={handleDeleteInstance}
onDelete={handleDeleteInstance}
onRestart={handleRestartInstance}
onLogout={handleLogoutInstance}
onSetPresence={handleSetPresence}
onRefreshInstances={handleRefreshInstances}
isRefreshing={isRefreshing}
/>
{/* QR Code Dialog */}

View File

@ -69,6 +69,121 @@ export const fetchConnectionState = async (instanceName: string): Promise<'open'
}
};
// Nova função para listar todas as instâncias
export const fetchAllInstances = async (): Promise<any> => {
try {
const response = await fetch(`https://${SERVER_URL}/fetch-instances`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'apikey': API_KEY
}
});
if (!response.ok) {
throw new Error('Erro ao buscar instâncias');
}
return response.json();
} catch (error) {
console.error("Error fetching all instances:", error);
throw error;
}
};
// Função para reiniciar uma instância
export const restartInstance = async (instanceName: string): Promise<any> => {
try {
const response = await fetch(`https://${SERVER_URL}/instance/restart/${encodeURIComponent(instanceName)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'apikey': API_KEY
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Erro ao reiniciar instância');
}
return response.json();
} catch (error) {
console.error(`Error restarting instance ${instanceName}:`, error);
throw error;
}
};
// Função para desconectar (logout) uma instância
export const logoutInstance = async (instanceName: string): Promise<any> => {
try {
const response = await fetch(`https://${SERVER_URL}/instance/logout/${encodeURIComponent(instanceName)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'apikey': API_KEY
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Erro ao desconectar instância');
}
return response.json();
} catch (error) {
console.error(`Error logging out instance ${instanceName}:`, error);
throw error;
}
};
// Função para excluir uma instância
export const deleteInstance = async (instanceName: string): Promise<any> => {
try {
const response = await fetch(`https://${SERVER_URL}/instance/${encodeURIComponent(instanceName)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'apikey': API_KEY
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Erro ao excluir instância');
}
return response.json();
} catch (error) {
console.error(`Error deleting instance ${instanceName}:`, error);
throw error;
}
};
// Função para definir presença online/offline
export const setInstancePresence = async (instanceName: string, presence: 'online' | 'offline'): Promise<any> => {
try {
const response = await fetch(`https://${SERVER_URL}/instance/setPresence/${encodeURIComponent(instanceName)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': API_KEY
},
body: JSON.stringify({ presence })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `Erro ao definir presença para ${presence}`);
}
return response.json();
} catch (error) {
console.error(`Error setting presence to ${presence} for instance ${instanceName}:`, error);
throw error;
}
};
export const saveInstancesToLocalStorage = (
instances: WhatsAppInstance[],
currentUserId: string

View File

@ -5,6 +5,7 @@ export interface WhatsAppInstance {
phoneNumber: string;
userId: string;
status?: string;
qrcode?: string;
qrcode?: string | null;
connectionState?: 'open' | 'closed' | 'connecting';
presence?: 'online' | 'offline';
}