import { useState, useEffect, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Link } from "wouter"; import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { MessageSquare, ArrowLeft, Search, MoreVertical, Send, Phone, Mail, Bot, Clock, Check, CheckCheck, Paperclip, Smile, Mic, Plus, Star, Archive, Trash2, Tag, Users, StickyNote, Zap, ArrowRightLeft, ChevronDown, Pin, X, Calendar, Settings, QrCode, Loader2, RefreshCw, LogOut, Wifi, WifiOff, Smartphone, BarChart3, MessageCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Textarea } from "@/components/ui/textarea"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { apiRequest } from "@/lib/queryClient"; import { toast } from "sonner"; import QRCode from "qrcode"; interface Conversation { id: number; contact_id: number; contact_name: string; contact_email: string; contact_avatar: string; contact_whatsapp: string; channel: string; status: string; priority: string; subject: string; last_message: string; messages_count: number; queue_id: number | null; queue_name: string | null; queue_color: string | null; created_at: string; updated_at: string; } interface Message { id: number; conversation_id: number; direction: string; sender_type: string; sender_name: string; content: string; content_type: string; attachments: any; read_at: string; delivered_at: string; created_at: string; } interface Queue { id: number; name: string; color: string; users_count: number; open_conversations: number; } interface InternalNote { id: number; content: string; user_name: string; is_pinned: boolean; created_at: string; } interface QuickMessage { id: number; shortcode: string; title: string; content: string; } interface WhatsAppStatus { status: "disconnected" | "connecting" | "qr_pending" | "connected"; qrCode: string | null; phoneNumber: string | null; } interface WhatsAppTicket { id: number; status: string; unreadCount: number; lastMessageAt: string | null; createdAt: string; contact: { id: number; whatsappId: string; name: string | null; pushName: string | null; phoneNumber: string | null; } | null; lastMessage: string | null; lastMessageFromMe: boolean; } interface WhatsAppMessage { id: number; messageId: string; body: string | null; fromMe: number; messageType: string; status: string | null; timestamp: string; } interface EmailAccount { id: number; email: string; displayName: string | null; provider: string; status: string; } interface EmailMessage { id: number; subject: string; fromAddress: string; fromName: string | null; snippet: string | null; isRead: number; isStarred: number; receivedAt: string | null; } type SidebarView = "conversations" | "channels"; export default function XosInbox() { const queryClient = useQueryClient(); const [selectedConversation, setSelectedConversation] = useState(null); const [messageText, setMessageText] = useState(""); const [filterChannel, setFilterChannel] = useState("all"); const [filterQueue, setFilterQueue] = useState("all"); const [showTransferDialog, setShowTransferDialog] = useState(false); const [showQuickMessages, setShowQuickMessages] = useState(false); const [showScheduleDialog, setShowScheduleDialog] = useState(false); const [newNoteContent, setNewNoteContent] = useState(""); const [transferQueueId, setTransferQueueId] = useState(""); const [scheduleDate, setScheduleDate] = useState(""); const [sidebarView, setSidebarView] = useState("conversations"); const [showConnectEmailDialog, setShowConnectEmailDialog] = useState(false); const [emailForm, setEmailForm] = useState({ email: "", password: "", provider: "gmail" }); const [qrDataUrl, setQrDataUrl] = useState(null); const [isPolling, setIsPolling] = useState(false); const [selectedWhatsAppTicket, setSelectedWhatsAppTicket] = useState(null); const [whatsappInput, setWhatsappInput] = useState(""); const { data: conversations = [] } = useQuery({ queryKey: ["/api/xos/conversations"], }); const { data: messages = [] } = useQuery({ queryKey: ["/api/xos/conversations", selectedConversation?.id, "messages"], queryFn: async () => { if (!selectedConversation) return []; const res = await fetch(`/api/xos/conversations/${selectedConversation.id}/messages`); return res.json(); }, enabled: !!selectedConversation, }); const { data: queues = [] } = useQuery({ queryKey: ["/api/xos/queues"], }); const { data: notes = [] } = useQuery({ queryKey: ["/api/xos/notes", { conversationId: selectedConversation?.id }], queryFn: async () => { if (!selectedConversation) return []; const res = await fetch(`/api/xos/notes?conversationId=${selectedConversation.id}`); return res.json(); }, enabled: !!selectedConversation, }); const { data: quickMessages = [] } = useQuery({ queryKey: ["/api/xos/quick-messages"], }); const { data: whatsappStatus, isLoading: loadingWhatsapp, refetch: refetchWhatsapp } = useQuery({ queryKey: ["whatsapp-status"], queryFn: async () => { const res = await fetch("/api/whatsapp/status", { credentials: "include" }); if (!res.ok) throw new Error("Failed to fetch status"); return res.json(); }, refetchInterval: isPolling ? 3000 : false, }); const { data: whatsappTickets = [], refetch: refetchTickets } = useQuery({ queryKey: ["whatsapp-tickets"], queryFn: async () => { const res = await fetch("/api/whatsapp/tickets", { credentials: "include" }); if (!res.ok) throw new Error("Failed to fetch tickets"); return res.json(); }, enabled: whatsappStatus?.status === "connected", refetchInterval: 10000, }); const { data: ticketMessages = [], refetch: refetchTicketMessages } = useQuery({ queryKey: ["ticket-messages", selectedWhatsAppTicket], queryFn: async () => { if (!selectedWhatsAppTicket) return []; const res = await fetch(`/api/whatsapp/tickets/${selectedWhatsAppTicket}/messages`, { credentials: "include" }); return res.json(); }, enabled: !!selectedWhatsAppTicket, refetchInterval: 5000, }); const { data: emailAccounts = [] } = useQuery({ queryKey: ["email-accounts"], queryFn: async () => { const res = await fetch("/api/email/accounts", { credentials: "include" }); if (!res.ok) throw new Error("Failed to fetch email accounts"); return res.json(); }, }); useEffect(() => { if (whatsappStatus?.status === "connected") { setIsPolling(false); setQrDataUrl(null); } else if (whatsappStatus?.status === "qr_pending" && whatsappStatus.qrCode) { QRCode.toDataURL(whatsappStatus.qrCode).then(setQrDataUrl); } }, [whatsappStatus]); const connectWhatsAppMutation = useMutation({ mutationFn: async () => { const res = await fetch("/api/whatsapp/connect", { method: "POST", credentials: "include" }); if (!res.ok) throw new Error("Failed to connect"); return res.json(); }, onSuccess: (data) => { if (data.status === "qr_pending" && data.qrCode) { QRCode.toDataURL(data.qrCode).then(setQrDataUrl); setIsPolling(true); } queryClient.invalidateQueries({ queryKey: ["whatsapp-status"] }); }, onError: () => toast.error("Erro ao conectar WhatsApp"), }); const disconnectWhatsAppMutation = useMutation({ mutationFn: async () => { const res = await fetch("/api/whatsapp/disconnect", { method: "POST", credentials: "include" }); if (!res.ok) throw new Error("Failed to disconnect"); }, onSuccess: () => { setIsPolling(false); setQrDataUrl(null); queryClient.invalidateQueries({ queryKey: ["whatsapp-status"] }); toast.success("WhatsApp desconectado"); }, }); const connectEmailMutation = useMutation({ mutationFn: async () => { const res = await fetch("/api/email/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(emailForm), credentials: "include", }); if (!res.ok) throw new Error("Failed to connect email"); return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["email-accounts"] }); setShowConnectEmailDialog(false); setEmailForm({ email: "", password: "", provider: "gmail" }); toast.success("Email conectado com sucesso!"); }, onError: () => toast.error("Erro ao conectar email. Verifique as credenciais."), }); const sendWhatsAppMutation = useMutation({ mutationFn: async ({ ticketId, message }: { ticketId: number; message: string }) => { const res = await fetch(`/api/whatsapp/tickets/${ticketId}/send`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }), credentials: "include", }); if (!res.ok) throw new Error("Failed to send message"); }, onSuccess: () => { setWhatsappInput(""); refetchTicketMessages(); queryClient.invalidateQueries({ queryKey: ["whatsapp-tickets"] }); }, onError: () => toast.error("Erro ao enviar mensagem"), }); const createNoteMutation = useMutation({ mutationFn: async (content: string) => { return apiRequest("POST", "/api/xos/notes", { conversationId: selectedConversation?.id, content, userName: "Atendente" }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/xos/notes"] }); setNewNoteContent(""); }, }); const transferMutation = useMutation({ mutationFn: async (queueId: string) => { return apiRequest("PUT", `/api/xos/conversations/${selectedConversation?.id}/transfer`, { queueId: parseInt(queueId) }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/xos/conversations"] }); setShowTransferDialog(false); }, }); const scheduleMutation = useMutation({ mutationFn: async (data: { content: string; scheduledAt: string }) => { return apiRequest("POST", "/api/xos/scheduled-messages", { conversationId: selectedConversation?.id, contactId: selectedConversation?.contact_id, content: data.content, scheduledAt: data.scheduledAt, }); }, onSuccess: () => { setShowScheduleDialog(false); setMessageText(""); setScheduleDate(""); }, }); const getChannelIcon = (channel: string) => { const icons: Record = { whatsapp: MessageSquare, email: Mail, chat: MessageSquare, phone: Phone, bot: Bot, }; return icons[channel] || MessageSquare; }; const getChannelColor = (channel: string) => { const colors: Record = { whatsapp: "bg-green-100 text-green-700", email: "bg-blue-100 text-blue-700", chat: "bg-indigo-100 text-indigo-700", phone: "bg-orange-100 text-orange-700", bot: "bg-purple-100 text-purple-700", }; return colors[channel] || "bg-gray-100 text-gray-700"; }; const getStatusColor = (status: string) => { const colors: Record = { open: "bg-yellow-100 text-yellow-800", pending: "bg-orange-100 text-orange-800", resolved: "bg-green-100 text-green-800", closed: "bg-gray-100 text-gray-800", }; return colors[status] || "bg-gray-100 text-gray-800"; }; const getQueueColorClass = (color: string) => { const colors: Record = { green: "bg-green-500", blue: "bg-blue-500", yellow: "bg-yellow-500", purple: "bg-purple-500", red: "bg-red-500", orange: "bg-orange-500", }; return colors[color] || "bg-slate-500"; }; const applyFilters = (convs: Conversation[]) => { return convs.filter((c) => { if (filterChannel !== "all" && c.channel !== filterChannel) return false; if (filterQueue !== "all" && c.queue_id?.toString() !== filterQueue) return false; return true; }); }; const formatTime = (date: string) => { const d = new Date(date); const now = new Date(); const diff = now.getTime() - d.getTime(); const hours = Math.floor(diff / (1000 * 60 * 60)); if (hours < 1) return "agora"; if (hours < 24) return `${hours}h`; if (hours < 48) return "ontem"; return d.toLocaleDateString("pt-BR"); }; const handleQuickMessageSelect = (qm: QuickMessage) => { setMessageText(qm.content); setShowQuickMessages(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "/" && messageText === "") { e.preventDefault(); setShowQuickMessages(true); } }; const handleSendWhatsApp = () => { if (!whatsappInput.trim() || !selectedWhatsAppTicket) return; sendWhatsAppMutation.mutate({ ticketId: selectedWhatsAppTicket, message: whatsappInput.trim() }); }; const getContactDisplayName = (ticket: WhatsAppTicket) => { if (!ticket.contact) return "Desconhecido"; return ticket.contact.name || ticket.contact.pushName || ticket.contact.phoneNumber || ticket.contact.whatsappId; }; const whatsappConversations: Conversation[] = whatsappTickets.map((ticket) => ({ id: 10000 + ticket.id, contact_id: ticket.contact?.id || 0, contact_name: getContactDisplayName(ticket), contact_email: "", contact_avatar: "", contact_whatsapp: ticket.contact?.phoneNumber || "", channel: "whatsapp", status: ticket.status, priority: ticket.unreadCount > 0 ? "high" : "normal", subject: "WhatsApp", last_message: ticket.lastMessage || "", messages_count: ticket.unreadCount, queue_id: null, queue_name: null, queue_color: null, created_at: ticket.createdAt, updated_at: ticket.lastMessageAt || ticket.createdAt, })); const sampleConversations: Conversation[] = [ { id: 1, contact_id: 1, contact_name: "João Silva", contact_email: "joao@empresa.com", contact_avatar: "", contact_whatsapp: "11999991111", channel: "whatsapp", status: "open", priority: "high", subject: "Dúvida sobre preços", last_message: "Olá, gostaria de saber mais sobre os planos do Arcádia Suite", messages_count: 5, queue_id: 1, queue_name: "Vendas", queue_color: "green", created_at: new Date(Date.now() - 3600000).toISOString(), updated_at: new Date(Date.now() - 300000).toISOString(), }, { id: 2, contact_id: 2, contact_name: "Maria Santos", contact_email: "maria@startup.com", contact_avatar: "", contact_whatsapp: "21988882222", channel: "email", status: "open", priority: "normal", subject: "Proposta comercial", last_message: "Segue em anexo a proposta que solicitou na última reunião", messages_count: 3, queue_id: 1, queue_name: "Vendas", queue_color: "green", created_at: new Date(Date.now() - 7200000).toISOString(), updated_at: new Date(Date.now() - 1800000).toISOString(), }, { id: 3, contact_id: 3, contact_name: "Carlos Oliveira", contact_email: "carlos@industria.com", contact_avatar: "", contact_whatsapp: "31977773333", channel: "chat", status: "pending", priority: "normal", subject: "Suporte técnico", last_message: "Estou com uma dúvida sobre a integração com o ERPNext", messages_count: 8, queue_id: 2, queue_name: "Suporte", queue_color: "blue", created_at: new Date(Date.now() - 86400000).toISOString(), updated_at: new Date(Date.now() - 3600000).toISOString(), }, ]; const sampleMessages: Message[] = [ { id: 1, conversation_id: 1, direction: "inbound", sender_type: "contact", sender_name: "João Silva", content: "Olá! Estou interessado no Arcádia Suite para minha empresa.", content_type: "text", attachments: null, read_at: new Date().toISOString(), delivered_at: new Date().toISOString(), created_at: new Date(Date.now() - 3600000).toISOString() }, { id: 2, conversation_id: 1, direction: "outbound", sender_type: "agent", sender_name: "Atendente", content: "Olá João! Seja bem-vindo. Ficarei feliz em ajudá-lo.", content_type: "text", attachments: null, read_at: new Date().toISOString(), delivered_at: new Date().toISOString(), created_at: new Date(Date.now() - 3500000).toISOString() }, ]; const sampleNotes: InternalNote[] = [ { id: 1, content: "Cliente pediu desconto de 15%", user_name: "Ana", is_pinned: true, created_at: new Date(Date.now() - 7200000).toISOString() }, ]; const sampleQueues: Queue[] = queues.length > 0 ? queues : [ { id: 1, name: "Vendas", color: "green", users_count: 3, open_conversations: 12 }, { id: 2, name: "Suporte", color: "blue", users_count: 5, open_conversations: 8 }, { id: 3, name: "Financeiro", color: "yellow", users_count: 2, open_conversations: 4 }, { id: 4, name: "Pós-Venda", color: "purple", users_count: 2, open_conversations: 3 }, ]; const sampleQuickMessages: QuickMessage[] = quickMessages.length > 0 ? quickMessages : [ { id: 1, shortcode: "/preco", title: "Tabela de Preços", content: "Segue nossa tabela de preços atualizada." }, { id: 2, shortcode: "/horario", title: "Horário", content: "Atendimento: segunda a sexta, 9h às 18h." }, { id: 3, shortcode: "/obrigado", title: "Agradecimento", content: "Obrigado pelo contato!" }, ]; const allConversations = [...(conversations.length > 0 ? conversations : sampleConversations), ...whatsappConversations]; const displayConversations = applyFilters(allConversations); const displayMessages = messages.length > 0 ? messages : (selectedConversation ? sampleMessages : []); const displayNotes = notes.length > 0 ? notes : (selectedConversation ? sampleNotes : []); const displayQueues = sampleQueues; const displayQuickMessages = sampleQuickMessages; const totalWhatsAppUnread = whatsappTickets.reduce((acc, t) => acc + t.unreadCount, 0); const channels = [ { id: "whatsapp", name: "WhatsApp", icon: Smartphone, color: "bg-green-500", connected: whatsappStatus?.status === "connected", count: totalWhatsAppUnread }, { id: "email", name: "Email", icon: Mail, color: "bg-blue-500", count: emailAccounts.length }, ]; return (

Inbox

Central de Atendimento Omnichannel

{whatsappStatus?.status === "connected" && ( WhatsApp Conectado )} {displayQueues.map((q) => ( setFilterQueue(filterQueue === q.id.toString() ? "all" : q.id.toString())}> {q.name} ({q.open_conversations}) ))}
setSidebarView(v as SidebarView)} className="w-full"> Conversas Canais
{sidebarView === "conversations" && ( <>
Todos WhatsApp Email Chat
Filas: {displayQueues.map((q) => ( setFilterQueue(filterQueue === q.id.toString() ? "all" : q.id.toString())} data-testid={`filter-queue-${q.id}`} > {q.name} ))}
{displayConversations.map((conv) => { const ChannelIcon = getChannelIcon(conv.channel); const isSelected = selectedConversation?.id === conv.id; return (
{ setSelectedConversation(conv); if (conv.channel === "whatsapp" && conv.id >= 10000) { setSelectedWhatsAppTicket(conv.id - 10000); } else { setSelectedWhatsAppTicket(null); } }} className={`p-3 cursor-pointer transition-colors hover:bg-slate-50 ${ isSelected ? "bg-blue-50 border-l-4 border-l-blue-500" : "" }`} data-testid={`conversation-${conv.id}`} >
{conv.contact_name.slice(0, 2).toUpperCase()} {conv.queue_color && ( )}

{conv.contact_name}

{formatTime(conv.updated_at)}

{conv.last_message}

{conv.status} {conv.priority === "high" && ( Urgente )}
); })}
)} {sidebarView === "channels" && (
Canais Conectados
WhatsApp {whatsappStatus?.status === "connected" ? ( Conectado ) : ( Desconectado )}
{whatsappStatus?.status === "connected" ? (
Telefone: {whatsappStatus.phoneNumber || "N/A"}
) : whatsappStatus?.status === "qr_pending" && qrDataUrl ? (

Escaneie o QR Code com seu WhatsApp

QR Code

Aguardando conexão...

) : ( )}
Email {emailAccounts.length > 0 ? `${emailAccounts.length} conta(s)` : "Nenhuma conta"}
{emailAccounts.length > 0 ? (
{emailAccounts.map((acc) => (
{acc.email} {acc.provider}
))}
) : ( )}
Chat Web Widget para seu site
)}
{selectedConversation ? ( <>
{selectedConversation.contact_name.slice(0, 2).toUpperCase()}

{selectedConversation.contact_name}

{selectedConversation.contact_email || selectedConversation.contact_whatsapp} {selectedConversation.channel}
Arquivar Adicionar tag Excluir
{selectedWhatsAppTicket ? ( ticketMessages.map((msg) => (

{msg.body}

{new Date(msg.timestamp).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} {msg.fromMe && }
)) ) : ( displayMessages.map((msg) => (
{msg.direction === "inbound" && (

{msg.sender_name}

)}

{msg.content}

{new Date(msg.created_at).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} {msg.direction === "outbound" && ( msg.read_at ? : )}
)) )}
Mensagens Rápidas
{displayQuickMessages.map((qm) => ( { if (selectedWhatsAppTicket) { setWhatsappInput(qm.content); } else { handleQuickMessageSelect(qm); } }}>

{qm.shortcode} - {qm.title}

{qm.content}

))}
{selectedWhatsAppTicket ? (