'use client'; /** * WhatsChatDrawer — painel slide-over (direita) com um clone FOCADO da área de * mensagens do NewWhats, aberto a partir de um agendamento da agenda. * * Diferente do InboxView (rota /wa-inbox), NÃO mostra a lista lateral de chats: * abre direto na conversa do paciente, resolvida pelo telefone do cadastro. * Um dropdown permite trocar entre o número do paciente e os do grupo familiar. * Se o paciente ainda não tem conversa, oferece iniciar uma (POST /conversations, * mesmo fluxo do NewConversationModal, que já funciona no satélite via proxy nw). * * Reaproveita a MESMA fiação de hooks do InboxView (stores zustand globais + * socket com guard de conexão), então montar este drawer não duplica a conexão. */ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, ChevronDown, User, Phone, Send, MessageCircle, Loader2, WifiOff, QrCode } from 'lucide-react'; import { HybridBackend } from '../../services/backend'; import { useChatStore } from './store/chatStore'; import { useInstanceStore } from './store/instanceStore'; import { formatPhone } from './utils/inboxUtils'; import { nw } from './services/nwClient'; import { useNewInboxBridge } from './hooks/inbox/useNewInboxBridge'; import { useInstanceSelector } from './hooks/inbox/useInstanceSelector'; import { useChatWindow } from './hooks/inbox/useChatWindow'; import { useMessageInput } from './hooks/inbox/useMessageInput'; import InboxHeader from './components/InboxHeader'; import MessageArea from './components/MessageArea'; import InputBar from './components/InputBar'; import ContactProfile from './components/ContactProfile'; // ─── Tipos ────────────────────────────────────────────────────────────────── export interface WhatsChatDrawerProps { isOpen: boolean; onClose: () => void; pacienteId?: string | null; pacienteNome?: string | null; onNavigate?: (view: string) => void; } interface NumeroOpcao { phone: string; // dígitos crus do cadastro nome: string; relacao: string; // 'Paciente' | grupofamiliarrelacao } // Normaliza para o formato de envio brasileiro (55 + DDD + número). function normalizeSendPhone(input: string): string { const digits = String(input || '').replace(/\D/g, ''); if (digits.startsWith('55') && (digits.length === 12 || digits.length === 13)) return digits; if (digits.length === 10 || digits.length === 11) return '55' + digits; return digits; } // Compara dois telefones de forma tolerante (Brasil: 9º dígito, DDI opcional): // casa pelos últimos 8 dígitos, que são estáveis. function samePhone(a: string, b: string): boolean { const da = String(a || '').replace(/\D/g, ''); const db = String(b || '').replace(/\D/g, ''); if (!da || !db) return false; return da.slice(-8) === db.slice(-8); } // ─── Conteúdo interno (só monta quando aberto → hooks/socket sob demanda) ────── const DrawerInner: React.FC> = ({ onClose, pacienteId, pacienteNome, onNavigate }) => { const { activeInstance, instances, connectedInstances } = useInstanceSelector(); const loadingInstances = useInstanceStore((s) => s.loading); // Há WhatsApp utilizável? (pelo menos uma instância conectada) const hasConnected = connectedInstances.length > 0; const { selectedChat, messages, loadingMessages, handleSendText, handleLoadMoreHistory, activeInstanceId, } = useNewInboxBridge(); const { scrollRef, isTyping, replyingTo, reactionPickerFor, hasMoreHistory, loadingMore, isProfileOpen, isSyncingAvatar, isChatMenuOpen, setReplyingTo, setIsProfileOpen, setIsChatMenuOpen, handleTyping, handleScroll, handleSelectChat: onChatWindowSelect, handleReactToMessage, handleSendMedia, handleSendAudio, handleSyncAvatar, toggleReactionPicker, } = useChatWindow({ selectedChat, activeInstanceId: activeInstance?.instance ?? null, onLoadMoreHistory: handleLoadMoreHistory, }); const { newMessage, setNewMessage, mentions, setMentions, handleSendMessage } = useMessageInput({ selectedChat, replyingTo, onClearReply: () => setReplyingTo(null), onSendText: handleSendText, }); // ── Números disponíveis (paciente + grupo familiar) ────────────────────── const [numeros, setNumeros] = useState([]); const [selectedPhone, setSelectedPhone] = useState(''); const [numDropdownOpen, setNumDropdownOpen] = useState(false); // ── Estado de resolução da conversa ────────────────────────────────────── const [resolving, setResolving] = useState(false); const [notFound, setNotFound] = useState(false); // número válido, mas sem conversa const [initText, setInitText] = useState(''); const [initSending, setInitSending] = useState(false); const [initError, setInitError] = useState(''); // Carrega telefone do paciente + membros da família ao abrir. useEffect(() => { let alive = true; (async () => { const opts: NumeroOpcao[] = []; const seen = new Set(); const push = (phone: string, nome: string, relacao: string) => { const d = String(phone || '').replace(/\D/g, ''); if (!d) return; const key = d.slice(-8); if (seen.has(key)) return; seen.add(key); opts.push({ phone: d, nome, relacao }); }; // 1) Telefone do próprio paciente — via cadastro (desambigua por id). if (pacienteNome) { try { const r = await HybridBackend.getPacientes(pacienteNome); const self = (r?.data || []).find((p: any) => p.id === pacienteId) ?? (r?.data || []).find((p: any) => (p.nome || '').toUpperCase() === pacienteNome.toUpperCase()); if (self?.telefone) push(self.telefone, self.nome || pacienteNome, 'Paciente'); } catch { /* ignore */ } } // 2) Grupo familiar / dependentes. if (pacienteId) { try { const { membros } = await HybridBackend.getFamiliaPaciente(pacienteId); for (const m of membros || []) { if (m.telefone) push(m.telefone, m.nome, m.grupofamiliarrelacao || (m.id === pacienteId ? 'Paciente' : 'Familiar')); } } catch { /* ignore */ } } if (!alive) return; setNumeros(opts); setSelectedPhone(opts[0]?.phone || ''); })(); return () => { alive = false; }; }, [pacienteId, pacienteNome]); // Resolve a conversa (chatId) a partir do telefone selecionado, sem enviar nada. const resolveChat = useCallback(async (phoneDigits: string) => { const store = useChatStore.getState(); if (!activeInstanceId || !phoneDigits) { setNotFound(true); return; } setResolving(true); setNotFound(false); store.setActiveChat(null); try { const send = normalizeSendPhone(phoneDigits); // O GET /inbox?search= casa por `jid contains` no backend. await store.loadChats(activeInstanceId, { search: send }); const chats = useChatStore.getState().chats; const hit = chats.find((c) => samePhone(c.contactPhone || c.jid.split('@')[0], send)) ?? null; if (hit) { await store.selectChat(hit.id); onChatWindowSelect(); setNotFound(false); } else { setNotFound(true); } } catch { setNotFound(true); } finally { setResolving(false); } }, [activeInstanceId, onChatWindowSelect]); // (Re)resolve quando muda o número selecionado ou a instância fica pronta. useEffect(() => { if (selectedPhone && activeInstanceId) resolveChat(selectedPhone); }, [selectedPhone, activeInstanceId, resolveChat]); // Inicia conversa (paciente sem chat): mesmo fluxo do NewConversationModal. const handleIniciarConversa = useCallback(async () => { if (!activeInstanceId || !selectedPhone) return; if (!initText.trim()) { setInitError('Escreva uma mensagem para iniciar a conversa.'); return; } setInitSending(true); setInitError(''); try { const phone = normalizeSendPhone(selectedPhone); await nw.post('/conversations', { sessionId: activeInstanceId, phone, text: initText.trim() }); setInitText(''); // Dá um respiro para o motor registrar o chat e então re-resolve. await new Promise((r) => setTimeout(r, 900)); await resolveChat(selectedPhone); } catch (e: any) { setInitError(e?.message || 'Erro ao enviar. Verifique o número e tente novamente.'); } finally { setInitSending(false); } }, [activeInstanceId, selectedPhone, initText, resolveChat]); const selectedNumero = useMemo( () => numeros.find((n) => n.phone === selectedPhone) ?? null, [numeros, selectedPhone] ); const tituloContato = selectedNumero?.nome || pacienteNome || 'Paciente'; return ( <> {/* Backdrop */} {/* Painel */} {/* Barra superior do drawer: contato + dropdown de números + fechar */}
{numDropdownOpen && numeros.length > 1 && (

Número / grupo familiar

{numeros.map((n) => ( ))}
)}
{/* Corpo */}
{(loadingInstances && instances.length === 0) ? ( // ── Carregando estado das sessões ──────────────────────────────
Verificando conexão do WhatsApp…
) : !hasConnected ? ( // ── Nenhuma sessão de WhatsApp conectada ────────────────────────

{instances.length === 0 ? 'WhatsApp não conectado' : 'Sessão desconectada'}

{instances.length === 0 ? 'Nenhuma conta de WhatsApp está vinculada a esta clínica. Conecte um número para conversar com os pacientes direto da agenda.' : 'A sessão do WhatsApp está fora do ar no momento. Reconecte o número lendo o QR Code para retomar as conversas.'}

{onNavigate && ( )}
) : resolving ? (
Abrindo conversa…
) : !selectedPhone ? (

Este paciente não tem telefone no cadastro.

) : notFound ? ( // ── Sem conversa: iniciar uma nova ──────────────────────────────

Sem conversa com {tituloContato}

{formatPhone(normalizeSendPhone(selectedPhone))}