diff --git a/src/components/chat/chat-session-item.tsx b/src/components/chat/chat-session-item.tsx new file mode 100644 index 0000000..9abd94c --- /dev/null +++ b/src/components/chat/chat-session-item.tsx @@ -0,0 +1,107 @@ +"use client" + +import { cn } from "@/lib/utils" +import { MessageCircle, WifiOff } from "lucide-react" + +type ChatSession = { + ticketId: string + ticketRef: number + ticketSubject: string + sessionId: string + agentId: string + unreadCount: number + lastActivityAt: number + machineHostname?: string | null + machineOnline?: boolean +} + +type ChatSessionItemProps = { + session: ChatSession + isActive?: boolean + onClick: () => void +} + +function formatTime(timestamp: number) { + const now = Date.now() + const diff = now - timestamp + const minutes = Math.floor(diff / 60000) + const hours = Math.floor(diff / 3600000) + const days = Math.floor(diff / 86400000) + + if (minutes < 1) return "Agora" + if (minutes < 60) return `${minutes}min` + if (hours < 24) return `${hours}h` + if (days === 1) return "Ontem" + + return new Date(timestamp).toLocaleDateString("pt-BR", { + day: "2-digit", + month: "2-digit", + }) +} + +export function ChatSessionItem({ session, isActive, onClick }: ChatSessionItemProps) { + const hasUnread = session.unreadCount > 0 + + return ( + + ) +} diff --git a/src/components/chat/chat-session-list.tsx b/src/components/chat/chat-session-list.tsx new file mode 100644 index 0000000..e84f592 --- /dev/null +++ b/src/components/chat/chat-session-list.tsx @@ -0,0 +1,112 @@ +"use client" + +import { useMemo } from "react" +import { MessageCircle, X } from "lucide-react" +import { ChatSessionItem } from "./chat-session-item" + +type ChatSession = { + ticketId: string + ticketRef: number + ticketSubject: string + sessionId: string + agentId: string + unreadCount: number + lastActivityAt: number + machineHostname?: string | null + machineOnline?: boolean +} + +type ChatSessionListProps = { + sessions: ChatSession[] + activeTicketId?: string | null + onSelectSession: (ticketId: string) => void + onClose: () => void + onMinimize: () => void +} + +export function ChatSessionList({ + sessions, + activeTicketId, + onSelectSession, + onClose, + onMinimize, +}: ChatSessionListProps) { + // Ordenar: nao lidos primeiro, depois por ultima atividade (desc) + const sortedSessions = useMemo(() => { + return [...sessions].sort((a, b) => { + // Nao lidos primeiro + if (a.unreadCount > 0 && b.unreadCount === 0) return -1 + if (a.unreadCount === 0 && b.unreadCount > 0) return 1 + // Depois por ultima atividade (mais recente primeiro) + return b.lastActivityAt - a.lastActivityAt + }) + }, [sessions]) + + const totalUnread = sessions.reduce((sum, s) => sum + s.unreadCount, 0) + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Chats

+

+ {sessions.length} conversa{sessions.length !== 1 ? "s" : ""} ativa{sessions.length !== 1 ? "s" : ""} + {totalUnread > 0 && ( + + ({totalUnread} nao lida{totalUnread !== 1 ? "s" : ""}) + + )} +

+
+
+
+ + +
+
+ + {/* Lista de sessoes */} +
+ {sortedSessions.length === 0 ? ( +
+
+ +
+

Nenhum chat ativo

+

+ Inicie um chat em um ticket para comecar +

+
+ ) : ( + sortedSessions.map((session) => ( + onSelectSession(session.ticketId)} + /> + )) + )} +
+
+ ) +} diff --git a/src/components/chat/chat-widget.tsx b/src/components/chat/chat-widget.tsx index 5083ab5..ece49d7 100644 --- a/src/components/chat/chat-widget.tsx +++ b/src/components/chat/chat-widget.tsx @@ -8,13 +8,6 @@ import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" import { Button } from "@/components/ui/button" import { Spinner } from "@/components/ui/spinner" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" import { cn } from "@/lib/utils" import { toast } from "sonner" import { @@ -24,6 +17,7 @@ import { Minimize2, User, ChevronDown, + ChevronLeft, WifiOff, XCircle, Paperclip, @@ -34,16 +28,20 @@ import { Eye, Check, } from "lucide-react" +import { ChatSessionList } from "./chat-session-list" const MAX_MESSAGE_LENGTH = 4000 const MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024 // 5MB const MAX_ATTACHMENTS = 5 const STORAGE_KEY = "chat-widget-state" +type ViewMode = "list" | "chat" + type ChatWidgetState = { isOpen: boolean isMinimized: boolean activeTicketId: string | null + viewMode: ViewMode } function formatTime(timestamp: number) { @@ -315,6 +313,17 @@ export function ChatWidget() { } catch {} return null }) + const [viewMode, setViewMode] = useState(() => { + if (typeof window === "undefined") return "list" + try { + const saved = localStorage.getItem(STORAGE_KEY) + if (saved) { + const state = JSON.parse(saved) as ChatWidgetState + return state.viewMode ?? "list" + } + } catch {} + return "list" + }) const [draft, setDraft] = useState("") const [isSending, setIsSending] = useState(false) const [isEndingChat, setIsEndingChat] = useState(false) @@ -369,6 +378,7 @@ export function ChatWidget() { const state = JSON.parse(event.newValue) as ChatWidgetState setIsOpen(state.isOpen) setIsMinimized(state.isMinimized) + setViewMode(state.viewMode ?? "list") if (state.activeTicketId) { setActiveTicketId(state.activeTicketId) } @@ -387,20 +397,32 @@ export function ChatWidget() { isOpen, isMinimized, activeTicketId, + viewMode, } // Salvar no localStorage (isso dispara evento storage em outras abas automaticamente) try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) } catch {} - }, [isOpen, isMinimized, activeTicketId]) + }, [isOpen, isMinimized, activeTicketId, viewMode]) - // Auto-selecionar primeira sessão se nenhuma selecionada + // Auto-selecionar modo e sessao baseado na quantidade de sessoes useEffect(() => { - if (!activeTicketId && activeSessions && activeSessions.length > 0) { + if (!activeSessions) return + + if (activeSessions.length === 0) { + // Sem sessoes, limpar estado + setActiveTicketId(null) + setViewMode("list") + } else if (activeSessions.length === 1) { + // Apenas 1 sessao, ir direto para chat setActiveTicketId(activeSessions[0].ticketId) + setViewMode("chat") + } else if (!activeTicketId) { + // Multiplas sessoes mas nenhuma selecionada, mostrar lista + setViewMode("list") } - }, [activeTicketId, activeSessions]) + }, [activeSessions, activeTicketId]) // Auto-abrir o widget quando ESTE agente iniciar uma nova sessão de chat. // Nao roda na montagem inicial para nao sobrescrever o estado do localStorage. @@ -648,6 +670,16 @@ export function ChatWidget() { } } + // Handlers para navegacao lista/chat + const handleSelectSession = (ticketId: string) => { + setActiveTicketId(ticketId) + setViewMode("chat") + } + + const handleBackToList = () => { + setViewMode("list") + } + // Nao mostrar se esta no Tauri (usa o chat nativo) if (isTauriContext) return null @@ -670,106 +702,105 @@ export function ChatWidget() { {/* Widget aberto */} {isOpen && !isMinimized && (
- {/* Header - Estilo card da aplicação */} -
-
-
- -
-
-
-

Chat

- {/* Indicador online/offline */} - {liveChat?.hasMachine && ( - machineOnline ? ( - - - Online - - ) : ( - - - Offline - - ) - )} -
- {activeSession && ( -
- 1 ? ( + setIsOpen(false)} + onMinimize={() => setIsMinimized(true)} + /> + ) : ( + <> + {/* Header - Modo Chat */} +
+
+ {/* Botao voltar para lista (quando ha multiplas sessoes) */} + {activeSessions.length > 1 && ( + + )} +
+ +
+
+
+

+ {activeSession ? `#${activeSession.ticketRef}` : "Chat"} +

+ {/* Indicador online/offline */} + {liveChat?.hasMachine && ( + machineOnline ? ( + + + Online + + ) : ( + + + Offline + + ) + )} +
+ {activeSession && ( +
+ + {activeSession.ticketSubject} + + + {machineHostname && ( + + {machineHostname} + + )} +
)}
- )} +
+
+ {/* Botao encerrar chat */} + + + +
-
-
- {/* Botão encerrar chat */} - - - -
-
- - {/* Seletor de sessões (se mais de uma) */} - {activeSessions.length > 1 && ( -
- -
- )} {/* Aviso de máquina offline */} {liveChat?.hasMachine && !machineOnline && ( @@ -956,6 +987,8 @@ export function ChatWidget() { accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.csv" />
+ + )}
)}