sg-delivery-customer/app/hooks/useCustomerSupport.ts

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,
};
};