import { useState, useEffect, useRef, useCallback } from 'react'; import { io, Socket } from 'socket.io-client'; import { tokenManager } from '@services'; import { SupportTicket, SupportMessage, CreateTicketPayload, } from '@interfaces'; import { createTicketApi, getMyTicketsApi, getTicketDetailsApi, postMessageApi, reopenTicketApi, uploadSupportAttachmentApi, } from '../api/supportApi'; const BASE_URL = 'https://accb-115-187-33-94.ngrok-free.app'; const SUPPORT_SOCKET_URL = `${BASE_URL}/support`; export const useCustomerSupport = (ticketId?: string) => { const [tickets, setTickets] = useState([]); const [currentTicket, setCurrentTicket] = useState(null); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); const [isAgentTyping, setIsAgentTyping] = useState(false); const [socketConnected, setSocketConnected] = useState(false); const socketRef = useRef(null); // 1. Fetch all my tickets const fetchTickets = useCallback(async () => { setLoading(true); try { const data = await getMyTicketsApi(); setTickets(data); } catch (error) { console.error('[useCustomerSupport] fetchTickets error:', error); } finally { setLoading(false); } }, []); // 2. Fetch single ticket details const fetchTicketDetails = useCallback(async (id: string) => { setLoading(true); try { const data = await getTicketDetailsApi(id); setCurrentTicket(data); setMessages(data.messages || []); } catch (error) { console.error('[useCustomerSupport] fetchTicketDetails error:', error); } finally { setLoading(false); } }, []); // 3. Create ticket const createTicket = useCallback(async (payload: CreateTicketPayload) => { setLoading(true); try { const newTicket = await createTicketApi(payload); setTickets(prev => [newTicket, ...prev]); return newTicket; } catch (error) { console.error('[useCustomerSupport] createTicket error:', error); throw error; } finally { setLoading(false); } }, []); // 4. Send reply message const sendMessage = useCallback(async (id: string, text: string, attachments?: string[]) => { try { const newMsg = await postMessageApi(id, { message: text, attachments }); setMessages(prev => { if (prev.some(m => m.id === newMsg.id)) return prev; return [...prev, newMsg]; }); return newMsg; } catch (error) { console.error('[useCustomerSupport] sendMessage error:', error); throw error; } }, []); // 5. Reopen ticket const reopenTicket = useCallback(async (id: string) => { setLoading(true); try { const updatedTicket = await reopenTicketApi(id); setCurrentTicket(updatedTicket); return updatedTicket; } catch (error) { console.error('[useCustomerSupport] reopenTicket error:', error); throw error; } finally { setLoading(false); } }, []); // 6. Upload attachment const uploadAttachment = useCallback(async (file: { uri: string; name: string; type: string }) => { const formData = new FormData(); formData.append('file', { uri: file.uri, name: file.name, type: file.type, } as any); try { const response = await uploadSupportAttachmentApi(formData); return response; } catch (error) { console.error('[useCustomerSupport] uploadAttachment error:', error); throw error; } }, []); // 7. Client typing emits const emitTypingStart = useCallback(() => { if (socketRef.current && ticketId) { socketRef.current.emit('typing_start', { ticketId }); } }, [ticketId]); const emitTypingStop = useCallback(() => { if (socketRef.current && ticketId) { socketRef.current.emit('typing_stop', { ticketId }); } }, [ticketId]); // 8. Socket Connection and Event Listeners Lifecycle useEffect(() => { if (!ticketId) { if (socketRef.current) { socketRef.current.disconnect(); socketRef.current = null; setSocketConnected(false); } return; } let active = true; const setupSocket = async () => { const token = await tokenManager.getAccessToken(); if (!token || !active) return; const socket = io(SUPPORT_SOCKET_URL, { auth: { token }, transports: ['websocket'], reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 2000, }); socketRef.current = socket; socket.on('connect', () => { if (!active) return; setSocketConnected(true); console.log('[useCustomerSupport Socket] Connected to /support'); socket.emit('join_ticket_room', { ticketId }); }); socket.on('disconnect', (reason) => { if (!active) return; setSocketConnected(false); console.log('[useCustomerSupport Socket] Disconnected:', reason); }); socket.on('support:joined', (data) => { console.log('[useCustomerSupport Socket] Joined room status:', data); }); socket.on('support:new_message', (msg: SupportMessage) => { if (!active) return; if (msg.ticketId === ticketId) { setMessages(prev => { if (prev.some(m => m.id === msg.id)) return prev; return [...prev, msg]; }); } }); socket.on('support:status_changed', (data: { ticketId: string; status: any }) => { if (!active) return; if (data.ticketId === ticketId) { setCurrentTicket(prev => prev ? { ...prev, status: data.status } : null); } }); socket.on('support:typing_start', (data: { ticketId: string; role: string }) => { if (!active) return; if (data.ticketId === ticketId && data.role === 'ADMIN') { setIsAgentTyping(true); } }); socket.on('support:typing_stop', (data: { ticketId: string }) => { if (!active) return; if (data.ticketId === ticketId) { setIsAgentTyping(false); } }); }; setupSocket(); return () => { active = false; if (socketRef.current) { socketRef.current.emit('leave_ticket_room', { ticketId }); socketRef.current.off('connect'); socketRef.current.off('disconnect'); socketRef.current.off('support:joined'); socketRef.current.off('support:new_message'); socketRef.current.off('support:status_changed'); socketRef.current.off('support:typing_start'); socketRef.current.off('support:typing_stop'); socketRef.current.disconnect(); socketRef.current = null; setSocketConnected(false); } }; }, [ticketId]); return { tickets, currentTicket, messages, loading, isAgentTyping, socketConnected, fetchTickets, fetchTicketDetails, createTicket, sendMessage, reopenTicket, uploadAttachment, emitTypingStart, emitTypingStop, }; };