247 lines
7.0 KiB
TypeScript
247 lines
7.0 KiB
TypeScript
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<SupportTicket[]>([]);
|
|
const [currentTicket, setCurrentTicket] = useState<SupportTicket | null>(null);
|
|
const [messages, setMessages] = useState<SupportMessage[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [isAgentTyping, setIsAgentTyping] = useState<boolean>(false);
|
|
const [socketConnected, setSocketConnected] = useState<boolean>(false);
|
|
|
|
const socketRef = useRef<Socket | null>(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,
|
|
};
|
|
};
|