diff --git a/src/app/dashboard/support-chat/page.tsx b/src/app/dashboard/support-chat/page.tsx new file mode 100644 index 0000000..38e625a --- /dev/null +++ b/src/app/dashboard/support-chat/page.tsx @@ -0,0 +1,378 @@ +'use client'; + +import { useState, useEffect, useRef, useCallback } from 'react'; +import { supportChatService, getErrorMessage } from '@/services'; +import type { SupportChat, SupportMessage } from '@/services'; + +function getUserDisplayName(chat: SupportChat): string { + const profile = chat.user.userProfile || chat.user.agentProfile; + if (profile && (profile.firstName || profile.lastName)) { + return [profile.firstName, profile.lastName].filter(Boolean).join(' '); + } + return chat.user.email; +} + +function formatTime(dateString: string): string { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); +} + +function formatMessageTime(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +export default function SupportChatPage() { + const [chats, setChats] = useState([]); + const [selectedChat, setSelectedChat] = useState(null); + const [messages, setMessages] = useState([]); + const [messageInput, setMessageInput] = useState(''); + const [filter, setFilter] = useState<'OPEN' | 'CLOSED'>('OPEN'); + const [loading, setLoading] = useState(true); + const [messagesLoading, setMessagesLoading] = useState(false); + const [sending, setSending] = useState(false); + const [error, setError] = useState(''); + const messagesEndRef = useRef(null); + const pollIntervalRef = useRef(null); + + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + + const fetchChats = useCallback(async () => { + try { + const data = await supportChatService.getAllChats(filter); + setChats(data); + } catch (err) { + setError(getErrorMessage(err)); + } finally { + setLoading(false); + } + }, [filter]); + + const fetchMessages = useCallback(async (chatId: string) => { + try { + const data = await supportChatService.getMessages(chatId); + setMessages(data.messages); + setTimeout(scrollToBottom, 100); + } catch (err) { + setError(getErrorMessage(err)); + } + }, [scrollToBottom]); + + // Fetch chats on mount and filter change + useEffect(() => { + setLoading(true); + setSelectedChat(null); + setMessages([]); + fetchChats(); + }, [fetchChats]); + + // Polling for real-time updates + useEffect(() => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + } + + pollIntervalRef.current = setInterval(() => { + fetchChats(); + if (selectedChat) { + fetchMessages(selectedChat.id); + } + }, 5000); + + return () => { + if (pollIntervalRef.current) { + clearInterval(pollIntervalRef.current); + } + }; + }, [fetchChats, fetchMessages, selectedChat]); + + const handleSelectChat = async (chat: SupportChat) => { + setSelectedChat(chat); + setMessagesLoading(true); + setMessages([]); + try { + const data = await supportChatService.getMessages(chat.id); + setMessages(data.messages); + if (chat.adminUnreadCount > 0) { + await supportChatService.markAsRead(chat.id); + fetchChats(); + } + setTimeout(scrollToBottom, 100); + } catch (err) { + setError(getErrorMessage(err)); + } finally { + setMessagesLoading(false); + } + }; + + const handleSendMessage = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedChat || !messageInput.trim() || sending) return; + + setSending(true); + try { + await supportChatService.sendMessage(selectedChat.id, messageInput.trim()); + setMessageInput(''); + await fetchMessages(selectedChat.id); + fetchChats(); + } catch (err) { + setError(getErrorMessage(err)); + } finally { + setSending(false); + } + }; + + const handleCloseChat = async (chatId: string) => { + try { + await supportChatService.closeChat(chatId); + if (selectedChat?.id === chatId) { + setSelectedChat(null); + setMessages([]); + } + fetchChats(); + } catch (err) { + setError(getErrorMessage(err)); + } + }; + + return ( +
+
+

Support Chat

+

Manage user support conversations

+
+ + {error && ( +
+

{error}

+ +
+ )} + + {/* Filter Tabs */} +
+ + +
+ + {/* Two-Panel Layout */} +
+ {/* Left Panel - Chat List */} +
+
+

+ Conversations ({chats.length}) +

+
+
+ {loading ? ( +
+
+
+ ) : chats.length === 0 ? ( +
+ No {filter.toLowerCase()} conversations +
+ ) : ( + chats.map((chat) => ( + + )) + )} +
+
+ + {/* Right Panel - Messages */} +
+ {selectedChat ? ( + <> + {/* Chat Header */} +
+
+

+ {getUserDisplayName(selectedChat)} +

+

{selectedChat.user.email}

+
+ {selectedChat.status === 'OPEN' && ( + + )} +
+ + {/* Messages Area */} +
+ {messagesLoading ? ( +
+
+
+ ) : messages.length === 0 ? ( +
+ No messages yet +
+ ) : ( + messages.map((message) => { + const isAdmin = message.senderRole === 'ADMIN'; + return ( +
+
+

{message.content}

+

+ {formatMessageTime(message.createdAt)} +

+
+
+ ); + }) + )} +
+
+ + {/* Input Area */} + {selectedChat.status === 'OPEN' && ( +
+ setMessageInput(e.target.value)} + placeholder="Type a message..." + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm text-gray-900 bg-white placeholder:text-gray-500" + /> + +
+ )} + + {selectedChat.status === 'CLOSED' && ( +
+

This conversation has been closed

+
+ )} + + ) : ( +
+
+ + + +

Select a conversation to view messages

+
+
+ )} +
+
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index c02a53c..88fffe6 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -74,6 +74,20 @@ const menuItems = [ ), }, + { + name: 'Support Chat', + href: '/dashboard/support-chat', + icon: ( + + + + ), + }, ]; export default function Sidebar() { diff --git a/src/services/index.ts b/src/services/index.ts index c77b848..8b18efd 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -84,3 +84,7 @@ export type { FaqItem, FaqContent, } from './cms.service'; + +// Support Chat Service +export { supportChatService } from './support-chat.service'; +export type { SupportChat, SupportMessage, MessagesResponse } from './support-chat.service'; diff --git a/src/services/support-chat.service.ts b/src/services/support-chat.service.ts new file mode 100644 index 0000000..e3c6da8 --- /dev/null +++ b/src/services/support-chat.service.ts @@ -0,0 +1,64 @@ +import api, { ApiResponse } from './api'; + +interface SupportChat { + id: string; + userId: string; + status: 'OPEN' | 'CLOSED'; + lastMessageAt: string | null; + lastMessageText: string | null; + userUnreadCount: number; + adminUnreadCount: number; + createdAt: string; + updatedAt: string; + user: { + id: string; + email: string; + role: string; + userProfile: { firstName: string | null; lastName: string | null; avatar: string | null } | null; + agentProfile: { firstName: string | null; lastName: string | null; avatar: string | null } | null; + }; +} + +interface SupportMessage { + id: string; + chatId: string; + senderId: string; + senderRole: string; + content: string; + createdAt: string; +} + +interface MessagesResponse { + messages: SupportMessage[]; + pagination: { page: number; limit: number; total: number; pages: number }; +} + +class SupportChatService { + async getAllChats(status?: string): Promise { + const params = status ? { status } : {}; + const response = await api.get>('/support-chat/admin/all', { params }); + return response.data.data; + } + + async getMessages(chatId: string, page = 1): Promise { + const response = await api.get>(`/support-chat/${chatId}/messages`, { params: { page, limit: 50 } }); + return response.data.data; + } + + async sendMessage(chatId: string, content: string): Promise { + const response = await api.post>(`/support-chat/${chatId}/messages`, { content }); + return response.data.data; + } + + async markAsRead(chatId: string): Promise { + await api.patch(`/support-chat/${chatId}/read`); + } + + async closeChat(chatId: string): Promise { + const response = await api.patch>(`/support-chat/admin/${chatId}/close`); + return response.data.data; + } +} + +export const supportChatService = new SupportChatService(); +export type { SupportChat, SupportMessage, MessagesResponse };