import { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { apiChat, type ChatConversation, type ChatMessage, type ChatStudent } from '../api'; import { type StaffChatOpenDetail } from '../chatEvents'; import { useAuth } from '../auth/AuthContext'; import { onStaffChatMessage, playChatSound } from '../hooks/useStaffChatSocket'; export function ChatWidget() { const { staff } = useAuth(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [students, setStudents] = useState([]); const [conversations, setConversations] = useState([]); const [active, setActive] = useState(null); const [messages, setMessages] = useState([]); const [draft, setDraft] = useState(''); const [sending, setSending] = useState(false); const [pickerOpen, setPickerOpen] = useState(false); const [toast, setToast] = useState<{ title: string; body: string } | null>(null); const listRef = useRef(null); const activeRef = useRef(null); const openRef = useRef(false); useEffect(() => { activeRef.current = active; }, [active]); useEffect(() => { openRef.current = open; }, [open]); const scrollBottom = () => { requestAnimationFrame(() => { if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight; }); }; const totalUnread = conversations.reduce((n, c) => n + (c.unread || 0), 0); const loadConversations = useCallback(async () => { const res = await apiChat.listConversations(); setConversations(res.data); return res.data; }, []); const loadMessages = useCallback(async (studentRkId: number) => { const res = await apiChat.listMessages(studentRkId); setMessages(res.data); scrollBottom(); await loadConversations(); }, [loadConversations]); const searchStudents = useCallback(async (q: string) => { const res = await apiChat.searchStudents(q); setStudents(res.data); }, []); const showToast = (title: string, body: string) => { setToast({ title, body }); setTimeout(() => setToast(null), 4500); }; const handleIncoming = useCallback((msg: ChatMessage) => { const studentId = Number(msg.studentRkId); const current = activeRef.current; void loadConversations(); if (msg.senderRole === 'student') { playChatSound(); const preview = msg.body?.slice(0, 80) || 'Tin nhắn mới'; if (!openRef.current || !current || Number(current.studentRkId) !== studentId) { showToast('Sinh viên nhắn tin', preview); } } if (current && Number(current.studentRkId) === studentId) { setMessages((prev) => { if (prev.some((m) => m.id === msg.id)) return prev; return [...prev, msg]; }); scrollBottom(); } }, [loadConversations]); useEffect(() => { const off = onStaffChatMessage(handleIncoming); return () => { off(); }; }, [handleIncoming]); useEffect(() => { const onOpen = (e: Event) => { const detail = (e as CustomEvent).detail; if (!detail?.studentRkId) return; setOpen(true); setActive({ studentRkId: detail.studentRkId, fullName: detail.fullName, studentCode: detail.studentCode, email: detail.email || '', online: false, }); setPickerOpen(false); setQuery(''); loadMessages(detail.studentRkId).catch(console.error); }; window.addEventListener('staff-chat:open', onOpen); return () => window.removeEventListener('staff-chat:open', onOpen); }, [loadMessages]); useEffect(() => { if (!staff?.id) return; loadConversations().catch(console.error); const t = setInterval(() => loadConversations().catch(console.error), 5000); return () => clearInterval(t); }, [staff?.id, loadConversations]); const pickStudent = async (s: ChatStudent) => { setActive(s); setPickerOpen(false); setQuery(''); await loadMessages(s.studentRkId); }; const pickFromConversation = async (c: ChatConversation) => { await pickStudent({ studentRkId: c.studentRkId, fullName: c.fullName, studentCode: c.studentCode, email: '', online: false, }); }; useEffect(() => { if (!open || !staff?.id) return; loadConversations().catch(console.error); searchStudents('').catch(console.error); }, [open, staff?.id, loadConversations, searchStudents]); const send = async () => { if (!active || !draft.trim()) return; setSending(true); try { const res = await apiChat.sendMessage(active.studentRkId, draft.trim()); setMessages((prev) => [...prev, res.data]); setDraft(''); scrollBottom(); await loadConversations(); } finally { setSending(false); } }; if (!staff) return null; const dock = (
{toast && (
{toast.title} {toast.body}
)} {!open ? ( ) : (
Tin nhắn
{!active ? (

Chọn hội thoại bên trái hoặc tìm sinh viên để bắt đầu chat

) : ( <>
{active.fullName}
{active.studentCode}
{messages.map((m) => (
{m.body}
{new Date(m.createdAt).toLocaleString('vi-VN', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
))} {messages.length === 0 &&
Chưa có tin nhắn
}
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }} />
)}
)}
); return createPortal(dock, document.body); }