Files
scoreodonto.com/frontend/views/newwhats/SessionsView.tsx
T
VPS 4 Builder 3042ddca38 feat(newwhats): frontend do plugin (Inbox/Sessions/Secretária) em tela cheia
- /wa-inbox, /wa-sessions e /wa-secretaria rodam sem a sidebar do scoreodonto
  (isStandaloneView); cada tela tem seu "Voltar" (Secretária ganhou botão na ThinNav).
- SessionsView: banner "Você já possui acesso ao WhatsApp" quando há sessão ativa.
- avatares: usa a URL do CDN (contactAvatarUrl/instance.avatar) direto; Avatar
  exibe sem depender de version; self-heal no onError re-busca a foto atual.
- deps: framer-motion, immer. Dev override com HMR (docker-compose.dev.yml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:54:24 +02:00

659 lines
29 KiB
TypeScript

// SessionsView — página /sessions do motor NewWhats, portada para o satélite
// scoreodonto. Dados/realtime via shims (ext API + túnel WS). Adaptações: sem
// next/link (navegação via onNavigate), export nomeado, wrapper de fundo escuro.
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Wifi, Plus, RefreshCw, Loader2, QrCode, X,
CheckCircle2, AlertCircle, Trash2, LogOut, Phone,
ArrowLeft, Zap, WifiOff, Clock, Terminal, Download,
ChevronDown, ChevronRight,
} from 'lucide-react';
import { Button } from './components/ui';
import { useSessionsLogic } from './hooks/useSessionsLogic';
import { socketService } from './services/socketService';
import type { Instance } from './store/instanceStore';
// ─── Status config ────────────────────────────────────────────────────────────
const STATUS_CONFIG = {
CONNECTED: {
label: 'Conectado',
dot: 'bg-emerald-400',
badge: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',
icon: <Wifi className="w-4 h-4" />,
},
QR_PENDING: {
label: 'Aguardando QR',
dot: 'bg-amber-400 animate-pulse',
badge: 'bg-amber-500/10 text-amber-400 border-amber-500/20',
icon: <QrCode className="w-4 h-4" />,
},
CONNECTING: {
label: 'Conectando...',
dot: 'bg-blue-400 animate-pulse',
badge: 'bg-blue-500/10 text-blue-400 border-blue-500/20',
icon: <Loader2 className="w-4 h-4 animate-spin" />,
},
DISCONNECTED: {
label: 'Desconectado',
dot: 'bg-slate-500',
badge: 'bg-slate-500/10 text-slate-400 border-slate-500/20',
icon: <WifiOff className="w-4 h-4" />,
},
BANNED: {
label: 'Banido',
dot: 'bg-red-500',
badge: 'bg-red-500/10 text-red-400 border-red-500/20',
icon: <AlertCircle className="w-4 h-4" />,
},
};
// ─── Baileys Log Types ────────────────────────────────────────────────────────
interface BaileysLogEntry {
id: string;
ts: number;
event: string;
data: any;
}
const EVENT_COLOR: Record<string, string> = {
'connection.update': 'text-emerald-400 bg-emerald-500/15',
'messages.upsert': 'text-blue-400 bg-blue-500/15',
'contacts.upsert': 'text-purple-400 bg-purple-500/15',
'contacts.update': 'text-purple-300 bg-purple-500/10',
'chats.upsert': 'text-yellow-400 bg-yellow-500/15',
'chats.update': 'text-yellow-300 bg-yellow-500/10',
'groups.upsert': 'text-orange-400 bg-orange-500/15',
'groups.update': 'text-orange-300 bg-orange-500/10',
'messaging-history.set': 'text-cyan-400 bg-cyan-500/15',
'message': 'text-blue-300 bg-blue-500/10',
};
function eventColor(event: string): string {
return EVENT_COLOR[event] ?? 'text-slate-400 bg-slate-500/15';
}
function logSummary(entry: BaileysLogEntry): string {
const d = entry.data;
if (!d) return '—';
if (entry.event === 'connection.update') {
if (d.connection) return `${d.connection}`;
if (d.qr) return 'QR gerado';
}
if (entry.event === 'message' || entry.event === 'messages.upsert') {
const body = d.body ?? d.message?.conversation ?? d.message?.extendedTextMessage?.text;
if (body) return String(body).slice(0, 60);
const jid = d.remoteJid ?? d.key?.remoteJid;
if (jid) return jid;
}
if (typeof d === 'object') {
const count = Array.isArray(d) ? d.length : (d.count ?? d.chats ?? d.contacts ?? d.messages);
if (count !== undefined) return `${count} item(s)`;
const keys = Object.keys(d).slice(0, 3).join(', ');
return keys || '{}';
}
return String(d).slice(0, 60);
}
// ─── Log Row (expandable) ─────────────────────────────────────────────────────
function LogRow({ entry, index }: { entry: BaileysLogEntry; index: number }) {
const [open, setOpen] = useState(false);
const time = new Date(entry.ts).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
return (
<div className="border-b border-white/[0.04] last:border-0">
<button
onClick={() => setOpen(o => !o)}
className="w-full flex items-center gap-2 px-3 py-1.5 hover:bg-white/[0.03] text-left group transition-colors"
>
<span className="text-[9px] font-mono text-slate-700 w-5 shrink-0 text-right">{index + 1}</span>
<span className={`text-[9px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded-full shrink-0 whitespace-nowrap ${eventColor(entry.event)}`}>
{entry.event}
</span>
<span className="text-[11px] text-slate-400 truncate flex-1 font-mono">{logSummary(entry)}</span>
<span className="text-[9px] text-slate-700 shrink-0 font-mono">{time}</span>
<span className="shrink-0 text-slate-700 group-hover:text-slate-500">
{open ? <ChevronDown size={11} /> : <ChevronRight size={11} />}
</span>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.15 }}
className="overflow-hidden"
>
<pre className="mx-3 mb-2 p-2 bg-black/60 rounded-lg text-[10px] font-mono text-slate-300 overflow-x-auto whitespace-pre-wrap max-h-48 border border-white/5">
{JSON.stringify(entry.data, null, 2)}
</pre>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
// ─── QR Modal ─────────────────────────────────────────────────────────────────
function QrModal({
instanceId,
instanceName,
qrBase64,
onClose,
onNewQr,
}: {
instanceId: string;
instanceName: string;
qrBase64: string | null;
onClose: () => void;
onNewQr: (id: string) => Promise<void>;
}) {
const [expired, setExpired] = useState(false);
const [logs, setLogs] = useState<BaileysLogEntry[]>([]);
const logEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!qrBase64) return;
setExpired(false);
const t = setTimeout(() => setExpired(true), 60_000);
return () => clearTimeout(t);
}, [qrBase64]);
useEffect(() => {
const handler = (log: any) => {
if (log.sessionId !== instanceId) return;
setLogs(prev => {
const entry: BaileysLogEntry = {
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
ts: log.timestamp ? new Date(log.timestamp).getTime() : Date.now(),
event: log.event ?? 'unknown',
data: log.data,
};
return [...prev, entry].slice(-300);
});
};
socketService.on('baileys:log', handler);
return () => socketService.off('baileys:log', handler);
}, [instanceId]);
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [logs.length]);
const saveJson = useCallback(() => {
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `baileys-${instanceId}-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}, [logs, instanceId]);
return (
<div className="fixed inset-0 z-[120] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/80 backdrop-blur-sm cursor-pointer"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative bg-[#0d0f18] border border-white/10 rounded-[2rem] shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col overflow-hidden"
onClick={e => e.stopPropagation()}
>
<div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.06] shrink-0">
<div>
<h2 className="text-lg font-black text-white">Conectar WhatsApp</h2>
<p className="text-slate-500 text-xs mt-0.5">{instanceName}</p>
</div>
<button onClick={onClose} className="p-2 text-slate-500 hover:text-white bg-white/5 rounded-xl transition-colors">
<X className="w-4 h-4" />
</button>
</div>
<div className="flex flex-1 min-h-0">
<div className="w-72 shrink-0 flex flex-col items-center justify-center gap-5 p-6 border-r border-white/[0.06]">
<div className="bg-white/[0.02] border border-white/5 p-5 rounded-3xl w-full flex flex-col items-center justify-center min-h-[260px]">
{expired ? (
<div className="flex flex-col items-center gap-4">
<Clock className="w-10 h-10 text-slate-500" />
<p className="font-bold text-white text-sm">QR expirado</p>
<Button variant="primary" size="sm" onClick={async () => {
setExpired(false);
await onNewQr(instanceId);
}}>
Novo QR
</Button>
</div>
) : qrBase64 ? (
<div className="bg-white p-2.5 rounded-2xl shadow-xl">
<img src={qrBase64.startsWith('data:') ? qrBase64 : `data:image/png;base64,${qrBase64}`} alt="QR Code" className="w-48 h-48 rounded-xl" />
</div>
) : (
<div className="flex flex-col items-center gap-3">
<Loader2 className="w-10 h-10 animate-spin text-emerald-500" />
<p className="text-xs text-slate-500 font-semibold uppercase tracking-widest">Gerando QR...</p>
</div>
)}
</div>
{!expired && (
<div className="flex items-center gap-2 text-emerald-400 text-[10px] font-bold uppercase tracking-widest animate-pulse">
<RefreshCw className="w-3.5 h-3.5 animate-spin" /> Aguardando pareamento...
</div>
)}
<button onClick={onClose} className="w-full py-2.5 text-slate-500 hover:text-white font-bold transition-colors text-sm">
Fechar
</button>
</div>
<div className="flex-1 flex flex-col min-w-0">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-white/[0.06] bg-black/20 shrink-0">
<Terminal className="w-3.5 h-3.5 text-slate-500" />
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500">Console Baileys</span>
<span className="ml-1 text-[9px] font-mono text-slate-700 bg-white/5 px-1.5 py-0.5 rounded-full">
{logs.length} eventos
</span>
<div className="ml-auto flex items-center gap-2">
<button
onClick={() => setLogs([])}
className="text-[9px] font-bold uppercase tracking-wider text-slate-600 hover:text-slate-400 transition-colors px-2 py-0.5 rounded hover:bg-white/5"
>
Limpar
</button>
<button
onClick={saveJson}
disabled={logs.length === 0}
className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-emerald-500 hover:text-emerald-400 disabled:opacity-30 disabled:cursor-not-allowed transition-colors px-2 py-0.5 rounded hover:bg-emerald-500/10"
>
<Download size={10} /> Salvar JSON
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto">
{logs.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-slate-700 gap-2 py-16">
<Terminal size={28} className="opacity-30" />
<p className="text-xs">Aguardando eventos do Baileys...</p>
</div>
) : (
<div>
{logs.map((entry, i) => (
<LogRow key={entry.id} entry={entry} index={i} />
))}
<div ref={logEndRef} />
</div>
)}
</div>
<div className="px-4 py-2 border-t border-white/[0.06] bg-black/20 shrink-0">
<div className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[9px] font-mono text-slate-600">
{instanceId.slice(0, 8)}... · live
</span>
</div>
</div>
</div>
</div>
</motion.div>
</div>
);
}
// ─── Session Card ─────────────────────────────────────────────────────────────
const SessionCard = React.forwardRef<HTMLDivElement, {
inst: Instance;
onConnect: () => void;
onDisconnect: () => void;
onDelete: () => void;
onShowQr: () => void;
}>(function SessionCard({
inst,
onConnect,
onDisconnect,
onDelete,
onShowQr,
}, ref) {
const cfg = STATUS_CONFIG[inst.status] ?? STATUS_CONFIG.DISCONNECTED;
const isConnected = inst.status === 'CONNECTED';
const isQrPending = inst.status === 'QR_PENDING';
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 15 }} animate={{ opacity: 1, y: 0 }}
className="bg-white/[0.03] backdrop-blur-xl p-6 rounded-[2rem] shadow-2xl border border-white/5 flex flex-col gap-5 hover:border-white/10 transition-all group relative overflow-hidden"
>
<div className="absolute top-0 right-0 w-32 h-32 bg-brand-500/5 blur-[60px] rounded-full translate-x-1/2 -translate-y-1/2 pointer-events-none" />
<div className="flex items-start justify-between relative z-10">
<div className="flex items-center gap-4">
<div className={`w-14 h-14 rounded-2xl overflow-hidden flex items-center justify-center border-2 shadow-lg ${isConnected ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400' : 'bg-white/5 border-white/10 text-slate-500'}`}>
{inst.avatar
? <img src={inst.avatar} alt={inst.name} className="w-full h-full object-cover" onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; (e.currentTarget.nextSibling as HTMLElement)?.style.setProperty('display', 'flex') }} />
: null}
<span className={`w-full h-full items-center justify-center ${inst.avatar ? 'hidden' : 'flex'}`}>
<Phone className="w-6 h-6" />
</span>
</div>
<div>
<h3 className="font-black text-white text-lg tracking-tight">{inst.name}</h3>
<div className="flex items-center gap-2 mt-1 flex-wrap">
{inst.phone && (
<span className="text-xs font-mono text-slate-500 bg-white/5 px-2 py-0.5 rounded border border-white/5">{inst.phone}</span>
)}
<span className={`inline-flex items-center gap-1.5 text-[10px] font-black uppercase tracking-wider px-2 py-0.5 rounded-full border ${cfg.badge}`}>
<span className={`w-1.5 h-1.5 rounded-full ${cfg.dot}`} />
{cfg.label}
</span>
</div>
</div>
</div>
<button onClick={onDelete} title="Deletar instância"
className="p-2 text-slate-600 hover:text-red-400 hover:bg-red-500/10 rounded-xl transition-colors shrink-0"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
{isQrPending && (
<button onClick={onShowQr}
className="flex items-center gap-3 p-3 bg-amber-500/10 border border-amber-500/20 rounded-2xl text-amber-300 text-sm font-semibold hover:bg-amber-500/15 transition-colors w-full"
>
<QrCode className="w-5 h-5 shrink-0" />
<span className="text-left">QR aguardando escaneamento clique para ver</span>
</button>
)}
<div className="flex items-center gap-2 mt-auto relative z-10 pt-2 border-t border-white/5">
{isConnected ? (
<>
<div className="flex items-center gap-2 text-emerald-400 text-xs font-bold uppercase tracking-widest flex-1">
<RefreshCw className="w-3.5 h-3.5 animate-spin" /> Sessão ativa
</div>
<button onClick={onDisconnect}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-semibold text-slate-400 hover:text-white bg-white/5 hover:bg-white/10 rounded-xl transition-colors border border-white/5"
>
<LogOut className="w-3.5 h-3.5" /> Desconectar
</button>
</>
) : (
<button onClick={onConnect}
className="flex-1 flex items-center justify-center gap-2 bg-white text-black font-black py-3 rounded-xl hover:bg-slate-200 transition-all text-[11px] uppercase tracking-widest"
>
<Zap className="w-3.5 h-3.5" fill="currentColor" /> Conectar
</button>
)}
</div>
</motion.div>
);
});
// ─── Confirm Delete Modal ─────────────────────────────────────────────────────
function ConfirmDeleteModal({
inst,
onClose,
onConfirm,
loading,
}: {
inst: Instance;
onClose: () => void;
onConfirm: () => void;
loading: boolean;
}) {
return (
<div className="fixed inset-0 z-[130] flex items-center justify-center p-4">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={onClose} className="absolute inset-0 bg-black/80 backdrop-blur-sm cursor-pointer" />
<motion.div initial={{ opacity: 0, scale: 0.92, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9 }}
className="relative bg-black/40 backdrop-blur-2xl border border-red-500/20 border-t-[3px] border-t-red-500 rounded-[28px] p-7 w-full max-w-sm shadow-2xl"
>
<div className="flex items-center gap-3 mb-4 mt-2">
<div className="w-11 h-11 rounded-xl bg-red-500/15 border border-red-500/30 flex items-center justify-center">
<Trash2 className="w-5 h-5 text-red-500" />
</div>
<div>
<h3 className="font-bold text-white">Deletar instância</h3>
<p className="text-xs text-slate-500">{inst.name}</p>
</div>
</div>
<p className="text-sm text-slate-400 mb-6 leading-relaxed">
Isso removerá permanentemente a instância, seus chats e sessão Baileys.<br />
<strong className="text-red-400">Esta ação é irreversível.</strong>
</p>
<div className="flex gap-3">
<Button variant="ghost" onClick={onClose} className="flex-1">Cancelar</Button>
<button onClick={onConfirm} disabled={loading}
className="flex-1 py-3 bg-red-500 hover:bg-red-600 text-white font-bold rounded-xl transition-colors disabled:opacity-50 flex items-center justify-center gap-2 text-sm"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
Deletar
</button>
</div>
</motion.div>
</div>
);
}
// ─── Main View ────────────────────────────────────────────────────────────────
export function SessionsView({ onNavigate }: { onNavigate?: (view: string) => void }) {
const {
instances,
loading,
toast,
addOpen,
newName,
creating,
setNewName,
openAddModal,
closeAddModal,
handleCreate,
qrModal,
closeQrModal,
handleNewQr,
deleteTarget,
deleting,
setDeleteTarget,
closeDeleteModal,
handleDelete,
handleConnect,
handleDisconnect,
handleShowQr,
refreshInstances,
} = useSessionsLogic();
// Já existe uma sessão ativa no motor para esta conta → mostra mensagem de
// sucesso em vez de sugerir novo pareamento.
const hasActiveSession = instances.some((i) => i.status === 'CONNECTED');
return (
<div className="bg-[#070b14] min-h-full text-slate-200">
<div className="max-w-5xl mx-auto p-6 lg:p-10 pb-20 font-sans space-y-10">
<AnimatePresence>
{toast && (
<motion.div
initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }}
className={`fixed top-6 left-1/2 -translate-x-1/2 z-[200] flex items-center gap-2.5 px-5 py-3 rounded-2xl shadow-2xl border text-sm font-semibold ${toast.ok ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-300' : 'bg-red-500/10 border-red-500/20 text-red-300'}`}
>
{toast.ok ? <CheckCircle2 className="w-4 h-4" /> : <AlertCircle className="w-4 h-4" />}
{toast.msg}
</motion.div>
)}
</AnimatePresence>
<header className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 relative">
<div className="absolute -top-16 -left-16 w-56 h-56 bg-brand-500/10 blur-[100px] rounded-full pointer-events-none" />
<div className="relative z-10 flex items-start gap-4">
<button onClick={() => onNavigate?.('dashboard')} className="p-2 hover:bg-white/5 rounded-xl transition-colors text-slate-400 hover:text-white mt-1">
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex-1">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-brand-500 to-brand-700 rounded-xl flex items-center justify-center text-white shadow-glow-brand shrink-0">
<Wifi className="w-6 h-6" />
</div>
<h1 className="text-3xl font-black text-white tracking-tight">Instâncias</h1>
</div>
<p className="text-sm text-slate-500 font-medium mt-1 ml-[60px]">WhatsApp Multi-Device · Baileys</p>
</div>
</div>
<div className="flex items-center gap-3 relative z-10">
<button onClick={refreshInstances} title="Recarregar"
className="p-3 bg-white/5 border border-white/10 rounded-xl text-slate-400 hover:text-white transition-all hover:bg-white/10"
>
<RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
</button>
<button onClick={openAddModal}
className="flex items-center gap-2 px-5 py-3 bg-brand-500 hover:bg-brand-600 text-white font-bold rounded-xl transition-all shadow-glow-brand text-sm"
>
<Plus className="w-5 h-5" /> Nova Instância
</button>
</div>
</header>
{hasActiveSession && (
<motion.div
initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}
className="flex items-center gap-4 p-5 rounded-2xl bg-emerald-500/10 border border-emerald-500/20"
>
<div className="w-11 h-11 rounded-xl bg-emerald-500/15 flex items-center justify-center text-emerald-400 shrink-0">
<CheckCircle2 className="w-6 h-6" />
</div>
<div className="flex-1 min-w-0">
<h2 className="text-base font-black text-emerald-300">Você possui acesso ao WhatsApp</h2>
<p className="text-sm text-emerald-400/70 font-medium">Sua sessão está ativa e conectada não é preciso parear novamente.</p>
</div>
{onNavigate && (
<button onClick={() => onNavigate('wa-inbox')}
className="shrink-0 flex items-center gap-2 px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white font-bold rounded-xl transition-all text-sm"
>
Abrir conversas
</button>
)}
</motion.div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
<AnimatePresence mode="popLayout">
{loading ? (
Array(3).fill(0).map((_, i) => (
<div key={i} className="h-56 rounded-[2rem] animate-pulse bg-white/5" />
))
) : instances.length === 0 ? (
<motion.div key="empty" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}
className="col-span-full py-24 flex flex-col items-center justify-center text-center gap-6"
>
<div className="w-20 h-20 bg-slate-800 rounded-full flex items-center justify-center text-slate-600">
<Phone className="w-10 h-10" />
</div>
<div>
<h2 className="text-xl font-bold text-white">Nenhuma instância</h2>
<p className="text-slate-500 mt-1 text-sm">Crie uma para começar a usar o WhatsApp.</p>
</div>
<button onClick={openAddModal}
className="flex items-center gap-2 px-6 py-3 bg-white/5 hover:bg-white/10 text-white font-bold rounded-2xl border border-white/10 transition-all"
>
<Plus className="w-5 h-5" /> Criar instância
</button>
</motion.div>
) : (
instances.map((inst) => (
<SessionCard
key={inst.id}
inst={inst}
onConnect={() => handleConnect(inst)}
onDisconnect={() => handleDisconnect(inst)}
onDelete={() => setDeleteTarget(inst)}
onShowQr={() => handleShowQr(inst)}
/>
))
)}
</AnimatePresence>
</div>
<AnimatePresence>
{addOpen && (
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
onClick={closeAddModal} className="absolute inset-0 bg-black/80 backdrop-blur-sm cursor-pointer" />
<motion.div initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative bg-white/[0.03] backdrop-blur-2xl p-8 rounded-[3rem] border border-white/10 max-w-sm w-full space-y-6 shadow-2xl"
>
<div>
<h2 className="text-2xl font-black text-white">Nova Instância</h2>
<p className="text-slate-500 text-sm mt-1"> um nome para identificar esta conexão.</p>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-slate-600 uppercase tracking-widest">Nome</label>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleCreate(); }}
placeholder="ex: principal, comercial, suporte..."
autoFocus
className="w-full bg-black/30 border border-white/10 rounded-2xl px-4 py-3 text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-brand-500/40 text-sm"
/>
</div>
<div className="flex gap-3">
<Button variant="ghost" onClick={closeAddModal} className="flex-1">
Cancelar
</Button>
<button onClick={handleCreate} disabled={creating || !newName.trim()}
className="flex-1 py-3 bg-emerald-500 hover:bg-emerald-600 text-white font-bold rounded-2xl transition-all disabled:opacity-50 flex items-center justify-center gap-2 text-sm"
>
{creating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
Criar e conectar
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<AnimatePresence>
{qrModal && (
<QrModal
instanceId={qrModal.instanceId}
instanceName={qrModal.instanceName}
qrBase64={qrModal.qrBase64}
onClose={closeQrModal}
onNewQr={handleNewQr}
/>
)}
</AnimatePresence>
<AnimatePresence>
{deleteTarget && (
<ConfirmDeleteModal
inst={deleteTarget}
onClose={closeDeleteModal}
onConfirm={handleDelete}
loading={deleting}
/>
)}
</AnimatePresence>
</div>
</div>
);
}
export default SessionsView;