import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Send, User, ShieldCheck, Bot, MessageSquare, Circle, Loader2 } from 'lucide-react'; import { supabase } from "../../lib/supabaseClient"; import useAuthStore from "../../store/authStore"; const TicketChat = ({ ticketId, currentUserRole = 'user' }) => { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(''); const [isTyping, setIsTyping] = useState(false); const [loading, setLoading] = useState(true); // eslint-disable-next-line no-unused-vars const [unreadCount, setUnreadCount] = useState(0); const [isAtBottom, setIsAtBottom] = useState(true); const [isInternal, setIsInternal] = useState(false); const [isStaff, setIsStaff] = useState(false); const { user, profile } = useAuthStore(); const messagesEndRef = useRef(null); const scrollContainerRef = useRef(null); const inputRef = useRef(null); const channelRef = useRef(null); // ─── Fetch Messages ────────────────────────────────────────────────── const fetchMessages = async () => { if (!ticketId) return; setLoading(true); try { // 1. Fetch Public Messages const { data: publicMsgs, error: pubErr } = await supabase .from('ticket_messages') .select('*') .eq('ticket_id', ticketId) .order('created_at', { ascending: true }); if (pubErr) throw pubErr; // 2. Fetch Internal Notes if staff let internalMsgs = []; const userRole = profile?.role || 'user'; const isStaffUser = ['admin', 'super_admin', 'agent', 'master_admin'].includes(userRole); setIsStaff(isStaffUser); if (isStaffUser) { const { data: privateMsgs, error: privErr } = await supabase .from('internal_notes') .select('*, profiles:agent_id(full_name)') .eq('ticket_id', ticketId) .order('created_at', { ascending: true }); if (!privErr && privateMsgs) { internalMsgs = privateMsgs.map(m => ({ ...m, message: m.content, sender_name: m.profiles?.full_name || 'Agent', sender_role: 'admin', is_internal: true })); } } // 3. Combine and Sort const combined = [...(publicMsgs || []), ...internalMsgs].sort( (a, b) => new Date(a.created_at) - new Date(b.created_at) ); setMessages(combined); } catch (err) { console.error("Error fetching messages:", err); } finally { setLoading(false); setTimeout(() => scrollToBottom(false), 50); } }; // ─── Realtime Subscription ─────────────────────────────────────────── useEffect(() => { fetchMessages(); const channel = supabase.channel(`ticket_chat_${ticketId}`); channelRef.current = channel; // Subscribe to changes channel .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'ticket_messages', filter: `ticket_id=eq.${ticketId}` }, (payload) => { const newMessage = payload.new; setMessages((prev) => { // Avoid duplicates if we already added it locally if (prev.find(m => m.id === newMessage.id)) return prev; // Remove optimistic duplicates based on content and time const filtered = prev.filter(m => !(String(m.id).startsWith('temp-') && m.message === newMessage.message && m.sender_id === newMessage.sender_id)); return [...filtered, newMessage]; }); // Handle notification logic if (newMessage.sender_id !== user?.id) { if (!isAtBottom) { setUnreadCount(prev => prev + 1); } else { setTimeout(() => scrollToBottom(), 50); } } } ) .on( 'broadcast', { event: 'typing' }, (payload) => { if (payload.payload.user_id !== user?.id) { setIsTyping(true); clearTimeout(window.typingTimeout); window.typingTimeout = setTimeout(() => { setIsTyping(false); }, 3000); setTimeout(() => scrollToBottom(), 50); } } ) .subscribe(); return () => { supabase.removeChannel(channel); }; }, [ticketId]); const handleInputChange = (e) => { setInputValue(e.target.value); if (channelRef.current && e.target.value.trim().length > 0) { channelRef.current.send({ type: 'broadcast', event: 'typing', payload: { user_id: user?.id } }).catch(() => { }); } }; // ─── Auto-scroll ───────────────────────────────────────────────────── const scrollToBottom = useCallback((smooth = true) => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollTo({ top: scrollContainerRef.current.scrollHeight, behavior: smooth ? 'smooth' : 'instant' }); } }, []); const handleScroll = () => { const el = scrollContainerRef.current; if (!el) return; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40; setIsAtBottom(atBottom); if (atBottom) setUnreadCount(0); }; // ─── Send message ──────────────────────────────────────────────────── const handleSend = async (e) => { e.preventDefault(); if (!inputValue.trim() || !user) return; const content = inputValue.trim(); const currentIsInternal = isInternal; // Optimistic UI update const tempMessage = { id: `temp-${Date.now()}`, ticket_id: ticketId, sender_id: user.id, sender_name: profile?.full_name || user.email, sender_role: profile?.role || 'user', message: content, is_internal: currentIsInternal, created_at: new Date().toISOString() }; setMessages(prev => [...prev, tempMessage]); setInputValue(''); setTimeout(() => scrollToBottom(), 50); try { if (currentIsInternal) { const { error } = await supabase .from('internal_notes') .insert([{ ticket_id: ticketId, agent_id: user.id, content: content }]); if (error) throw error; } else { const { error } = await supabase .from('ticket_messages') .insert([{ ticket_id: ticketId, sender_id: user.id, sender_name: profile?.full_name || user.email, sender_role: profile?.role || 'user', message: content }]); if (error) throw error; } } catch (err) { console.error("Error sending message:", err); setMessages(prev => prev.filter(m => m.id !== tempMessage.id)); } }; // ─── Helpers ───────────────────────────────────────────────────────── const formatTime = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } }; const formatDate = (iso) => { try { const d = new Date(iso); const today = new Date(); if (d.toDateString() === today.toDateString()) return 'Today'; const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); if (d.toDateString() === yesterday.toDateString()) return 'Yesterday'; return d.toLocaleDateString([], { month: 'short', day: 'numeric' }); } catch { return ''; } }; const grouped = []; let lastDate = null; messages.forEach((msg) => { const date = formatDate(msg.created_at); if (date !== lastDate) { grouped.push({ type: 'divider', label: date }); lastDate = date; } grouped.push({ type: 'message', data: msg }); }); return (
{/* Header */}

AI ASSISTANT

{isStaff && (
)}
{/* Messages */}
{loading ? (
) : grouped.length === 0 ? (

No messages yet.

) : ( grouped.map((item, i) => { if (item.type === 'divider') { return (
{item.label}
); } const msg = item.data; const isMe = msg.sender_id === user?.id; const isAdmin = msg.sender_role === 'admin' || msg.sender_role === 'super_admin'; return (
{!isMe && (
{isAdmin ? : }
)}
{msg.is_internal ? '🔒 Internal Note' : (isMe ? 'You' : msg.sender_name || (isAdmin ? 'Support ' : 'User'))} {formatTime(msg.created_at)}
{msg.message}
{isMe && (
{isAdmin ? : }
)}
); }) )} {isTyping && (
Someone is typing...
)}
{/* Input */}
); }; export default TicketChat;