// 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: , }, QR_PENDING: { label: 'Aguardando QR', dot: 'bg-amber-400 animate-pulse', badge: 'bg-amber-500/10 text-amber-400 border-amber-500/20', icon: , }, CONNECTING: { label: 'Conectando...', dot: 'bg-blue-400 animate-pulse', badge: 'bg-blue-500/10 text-blue-400 border-blue-500/20', icon: , }, DISCONNECTED: { label: 'Desconectado', dot: 'bg-slate-500', badge: 'bg-slate-500/10 text-slate-400 border-slate-500/20', icon: , }, BANNED: { label: 'Banido', dot: 'bg-red-500', badge: 'bg-red-500/10 text-red-400 border-red-500/20', icon: , }, }; // ─── Baileys Log Types ──────────────────────────────────────────────────────── interface BaileysLogEntry { id: string; ts: number; event: string; data: any; } const EVENT_COLOR: Record = { '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 (
{open && (
              {JSON.stringify(entry.data, null, 2)}
            
)}
); } // ─── QR Modal ───────────────────────────────────────────────────────────────── function QrModal({ instanceId, instanceName, qrBase64, onClose, onNewQr, }: { instanceId: string; instanceName: string; qrBase64: string | null; onClose: () => void; onNewQr: (id: string) => Promise; }) { const [expired, setExpired] = useState(false); const [logs, setLogs] = useState([]); const logEndRef = useRef(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 (
e.stopPropagation()} >

Conectar WhatsApp

{instanceName}

{expired ? (

QR expirado

) : qrBase64 ? (
QR Code
) : (

Gerando QR...

)}
{!expired && (
Aguardando pareamento...
)}
Console Baileys {logs.length} eventos
{logs.length === 0 ? (

Aguardando eventos do Baileys...

) : (
{logs.map((entry, i) => ( ))}
)}
{instanceId.slice(0, 8)}... · live
); } // ─── Session Card ───────────────────────────────────────────────────────────── const SessionCard = React.forwardRef 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 (
{inst.avatar ? {inst.name} { (e.currentTarget as HTMLImageElement).style.display = 'none'; (e.currentTarget.nextSibling as HTMLElement)?.style.setProperty('display', 'flex') }} /> : null}

{inst.name}

{inst.phone && ( {inst.phone} )} {cfg.label}
{isQrPending && ( )}
{isConnected ? ( <>
Sessão ativa
) : ( )}
); }); // ─── Confirm Delete Modal ───────────────────────────────────────────────────── function ConfirmDeleteModal({ inst, onClose, onConfirm, loading, }: { inst: Instance; onClose: () => void; onConfirm: () => void; loading: boolean; }) { return (

Deletar instância

{inst.name}

Isso removerá permanentemente a instância, seus chats e sessão Baileys.
Esta ação é irreversível.

); } // ─── 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 (
{toast && ( {toast.ok ? : } {toast.msg} )}

Instâncias

WhatsApp Multi-Device · Baileys

{hasActiveSession && (

Você já possui acesso ao WhatsApp

Sua sessão está ativa e conectada — não é preciso parear novamente.

{onNavigate && ( )}
)}
{loading ? ( Array(3).fill(0).map((_, i) => (
)) ) : instances.length === 0 ? (

Nenhuma instância

Crie uma para começar a usar o WhatsApp.

) : ( instances.map((inst) => ( handleConnect(inst)} onDisconnect={() => handleDisconnect(inst)} onDelete={() => setDeleteTarget(inst)} onShowQr={() => handleShowQr(inst)} /> )) )}
{addOpen && (

Nova Instância

Dê um nome para identificar esta conexão.

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" />
)}
{qrModal && ( )} {deleteTarget && ( )}
); } export default SessionsView;