From 145d94adcdefafb3257340544e04817cc729f2d4 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 7 Feb 2026 05:30:31 +0300 Subject: [PATCH] feat: inline ticket chat in admin user detail Replace ticket navigation with inline chat view. Clicking a ticket opens the conversation directly within the user detail page with message history, media support, and reply form. --- src/pages/AdminUserDetail.tsx | 202 +++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 3 deletions(-) diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 8a40ddc..c1a9feb 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -11,7 +11,8 @@ import { type PanelSyncStatusResponse, type UpdateSubscriptionRequest, } from '../api/adminUsers'; -import { adminApi, type AdminTicket } from '../api/admin'; +import { adminApi, type AdminTicket, type AdminTicketDetail } from '../api/admin'; +import { ticketsApi } from '../api/tickets'; import { AdminBackButton } from '../components/admin'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; @@ -109,6 +110,12 @@ export default function AdminUserDetail() { const [tickets, setTickets] = useState([]); const [ticketsLoading, setTicketsLoading] = useState(false); const [ticketsTotal, setTicketsTotal] = useState(0); + const [selectedTicketId, setSelectedTicketId] = useState(null); + const [selectedTicket, setSelectedTicket] = useState(null); + const [ticketDetailLoading, setTicketDetailLoading] = useState(false); + const [replyText, setReplyText] = useState(''); + const [replySending, setReplySending] = useState(false); + const messagesEndRef = useRef(null); // Subscription form const [subAction, setSubAction] = useState('extend'); @@ -165,6 +172,45 @@ export default function AdminUserDetail() { } }, [userId]); + const loadTicketDetail = useCallback(async (ticketId: number) => { + try { + setTicketDetailLoading(true); + const data = await adminApi.getTicket(ticketId); + setSelectedTicket(data); + } catch (error) { + console.error('Failed to load ticket detail:', error); + } finally { + setTicketDetailLoading(false); + } + }, []); + + const handleTicketReply = async () => { + if (!selectedTicketId || !replyText.trim()) return; + setReplySending(true); + try { + await adminApi.replyToTicket(selectedTicketId, replyText); + setReplyText(''); + await loadTicketDetail(selectedTicketId); + await loadTickets(); + } catch (error) { + console.error('Failed to reply:', error); + } finally { + setReplySending(false); + } + }; + + useEffect(() => { + if (selectedTicketId) { + loadTicketDetail(selectedTicketId); + } + }, [selectedTicketId, loadTicketDetail]); + + useEffect(() => { + if (selectedTicket && messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [selectedTicket]); + useEffect(() => { if (!userId || isNaN(userId)) { navigate('/admin/users'); @@ -1037,7 +1083,157 @@ export default function AdminUserDetail() { {/* Tickets Tab */} {activeTab === 'tickets' && (
- {ticketsLoading ? ( + {selectedTicketId ? ( + /* Ticket Chat View */ + ticketDetailLoading ? ( +
+
+
+ ) : selectedTicket ? ( +
+ {/* Chat header */} +
+ +
+
+ #{selectedTicket.id} {selectedTicket.title} +
+
+ + {selectedTicket.status} + + {formatDate(selectedTicket.created_at)} +
+
+
+ + {/* Messages */} +
+ {selectedTicket.messages.map((msg) => ( +
+
+ + {msg.is_from_admin + ? t('admin.tickets.adminLabel') + : t('admin.tickets.userLabel')} + + + {formatDate(msg.created_at)} + +
+

+ {msg.message_text} +

+ {msg.has_media && msg.media_file_id && ( +
+ {msg.media_type === 'photo' ? ( + {msg.media_caption + ) : ( + + {msg.media_caption || msg.media_type} + + )} + {msg.media_caption && msg.media_type === 'photo' && ( +

{msg.media_caption}

+ )} +
+ )} +
+ ))} +
+
+ + {/* Reply form */} + {selectedTicket.status !== 'closed' && ( +
+