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 { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; 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 { WhatsAppInstance } from '@/types/whatsAppTypes';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
interface InstanceCardProps { interface InstanceCardProps {
instance: WhatsAppInstance; instance: WhatsAppInstance;
onViewQrCode: (instance: WhatsAppInstance) => void; onViewQrCode: (instance: WhatsAppInstance) => void;
onDelete: (instanceId: string) => 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 ( return (
<Card className="h-full"> <>
<CardHeader className="pb-2"> <Card className="h-full">
<CardTitle className="text-lg">{instance.instanceName}</CardTitle> <CardHeader className="pb-2">
</CardHeader> <CardTitle className="text-lg">{instance.instanceName}</CardTitle>
<CardContent className="pb-2"> </CardHeader>
<div className="space-y-2"> <CardContent className="pb-2">
<div className="flex items-center text-sm"> <div className="space-y-2">
<Smartphone className="h-4 w-4 mr-2 text-gray-500" /> <div className="flex items-center text-sm">
<span>{instance.phoneNumber}</span> <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>
<div className="flex items-center text-sm"> </CardContent>
{instance.connectionState === 'open' ? ( <CardFooter className="flex flex-col gap-2">
<Badge variant="success" className="flex items-center gap-1"> <div className="flex justify-between w-full">
<span className="inline-block w-2 h-2 rounded-full bg-green-500"></span> <Button
Status: Conectado variant="outline"
</Badge> size="sm"
) : ( onClick={() => onViewQrCode(instance)}
<Badge variant="outline" className="flex items-center gap-1 text-red-500 border-red-300"> className="flex items-center"
<span className="inline-block w-2 h-2 rounded-full bg-red-500"></span> >
Status: Desconectado <QrCode className="h-4 w-4 mr-2" />
</Badge> 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>
</div> <div className="grid grid-cols-2 gap-2 w-full">
</CardContent> <Button
<CardFooter className="flex justify-between"> variant="outline"
<Button size="sm"
variant="outline" onClick={handleRestart}
size="sm" disabled={loading !== null}
onClick={() => onViewQrCode(instance)} >
className="flex items-center" <RefreshCw className="h-4 w-4 mr-1" />
> Reiniciar
<QrCode className="h-4 w-4 mr-2" /> </Button>
Ver QR Code <Button
</Button> variant="outline"
<Button size="sm"
variant="ghost" onClick={handleLogout}
size="sm" disabled={loading !== null}
className="text-red-500 hover:text-red-700 hover:bg-red-50" >
onClick={() => onDelete(instance.instanceId)} <PowerOff className="h-4 w-4 mr-1" />
> Desconectar
Remover </Button>
</Button> </div>
</CardFooter> <div className="grid grid-cols-2 gap-2 w-full">
</Card> <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 { Card, CardContent } from '@/components/ui/card';
import InstanceCard from './InstanceCard'; import InstanceCard from './InstanceCard';
import { WhatsAppInstance } from '@/types/whatsAppTypes'; import { WhatsAppInstance } from '@/types/whatsAppTypes';
import { RefreshCw } from 'lucide-react';
interface InstanceListProps { interface InstanceListProps {
instances: WhatsAppInstance[]; instances: WhatsAppInstance[];
onViewQrCode: (instance: WhatsAppInstance) => void; onViewQrCode: (instance: WhatsAppInstance) => void;
onDelete: (instanceId: string) => 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) { if (instances.length === 0) {
return ( return (
<Card> <Card>
@ -25,7 +41,18 @@ const InstanceList = ({ instances, onViewQrCode, onDelete }: InstanceListProps)
return ( return (
<div className="space-y-4"> <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"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{instances.map((instance) => ( {instances.map((instance) => (
<InstanceCard <InstanceCard
@ -33,6 +60,9 @@ const InstanceList = ({ instances, onViewQrCode, onDelete }: InstanceListProps)
instance={instance} instance={instance}
onViewQrCode={onViewQrCode} onViewQrCode={onViewQrCode}
onDelete={onDelete} onDelete={onDelete}
onRestart={onRestart}
onLogout={onLogout}
onSetPresence={onSetPresence}
/> />
))} ))}
</div> </div>

View File

@ -7,7 +7,12 @@ import {
fetchQrCode, fetchQrCode,
fetchConnectionState, fetchConnectionState,
saveInstancesToLocalStorage, saveInstancesToLocalStorage,
loadInstancesFromLocalStorage loadInstancesFromLocalStorage,
fetchAllInstances,
restartInstance,
logoutInstance,
deleteInstance,
setInstancePresence
} from '@/services/whatsAppService'; } from '@/services/whatsAppService';
import CreateInstanceForm from '@/components/whatsapp/CreateInstanceForm'; import CreateInstanceForm from '@/components/whatsapp/CreateInstanceForm';
import InstanceList from '@/components/whatsapp/InstanceList'; import InstanceList from '@/components/whatsapp/InstanceList';
@ -20,6 +25,7 @@ const WhatsApp = () => {
const [qrDialogOpen, setQrDialogOpen] = useState(false); const [qrDialogOpen, setQrDialogOpen] = useState(false);
const [statusCheckInterval, setStatusCheckInterval] = useState<NodeJS.Timeout | null>(null); const [statusCheckInterval, setStatusCheckInterval] = useState<NodeJS.Timeout | null>(null);
const [currentUserId, setCurrentUserId] = useState<string>(''); const [currentUserId, setCurrentUserId] = useState<string>('');
const [isRefreshing, setIsRefreshing] = useState(false);
// Get current user ID and load instances on component mount // Get current user ID and load instances on component mount
useEffect(() => { 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 // Handler for when a new instance is created
const handleInstanceCreated = async (newInstance: WhatsAppInstance) => { const handleInstanceCreated = async (newInstance: WhatsAppInstance) => {
console.log("New instance created, adding to instances list:", newInstance); console.log("New instance created, adding to instances list:", newInstance);
@ -152,23 +307,49 @@ const WhatsApp = () => {
}; };
// Handler for when an instance is deleted // Handler for when an instance is deleted
const handleDeleteInstance = (instanceId: string) => { const handleDeleteInstance = async (instanceId: string) => {
console.log(`Deleting instance with ID: ${instanceId}`); 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 // Find the instance before deleting it
if (activeInstance?.instanceId === instanceId) { const instanceToDelete = instances.find(i => i.instanceId === instanceId);
setQrDialogOpen(false);
if (!instanceToDelete) {
toast({
title: "Erro",
description: "Instância não encontrada",
variant: "destructive",
});
return;
} }
toast({ try {
title: "Instância removida", // Call API to delete instance
description: "A instância do WhatsApp foi removida com sucesso.", 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 ( return (
@ -185,7 +366,12 @@ const WhatsApp = () => {
<InstanceList <InstanceList
instances={instances} instances={instances}
onViewQrCode={handleViewQrCode} onViewQrCode={handleViewQrCode}
onDelete={handleDeleteInstance} onDelete={handleDeleteInstance}
onRestart={handleRestartInstance}
onLogout={handleLogoutInstance}
onSetPresence={handleSetPresence}
onRefreshInstances={handleRefreshInstances}
isRefreshing={isRefreshing}
/> />
{/* QR Code Dialog */} {/* 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 = ( export const saveInstancesToLocalStorage = (
instances: WhatsAppInstance[], instances: WhatsAppInstance[],
currentUserId: string currentUserId: string

View File

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