import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { adminApi, type AdminTicket, type AdminTicketDetail } from '../../../api/admin'; import { MessageMediaGrid } from '../../tickets/MessageMediaGrid'; import { linkifyText } from '../../../utils/linkify'; import { ChatIcon, BackIcon, SendIcon } from '@/components/icons'; // ────────────────────────────────────────────────────────────────── // Tickets tab — list view + chat view (selected ticket replaces list). // Owns its own query, selection state, reply form state, and the // auto-scroll-to-bottom ref. Parent only knows the userId. // ────────────────────────────────────────────────────────────────── export interface TicketsTabProps { userId: number; formatDate: (date: string | null) => string; } const STATUS_VALUES = ['open', 'pending', 'answered', 'closed'] as const; type TicketStatus = (typeof STATUS_VALUES)[number]; export function TicketsTab({ userId, formatDate }: TicketsTabProps) { // List query const ticketsQuery = useQuery({ queryKey: ['admin-user-tickets', userId] as const, queryFn: () => adminApi.getTickets({ user_id: userId, per_page: 50 }), enabled: !!userId, }); const tickets = ticketsQuery.data?.items ?? []; const ticketsTotal = ticketsQuery.data?.total ?? 0; const ticketsLoading = ticketsQuery.isFetching; // Selected ticket (chat view) const [selectedTicketId, setSelectedTicketId] = useState(null); const [selectedTicket, setSelectedTicket] = useState(null); const [ticketDetailLoading, setTicketDetailLoading] = useState(false); // Reply form const [replyText, setReplyText] = useState(''); const [replySending, setReplySending] = useState(false); // Status-change gate const [actionLoading, setActionLoading] = useState(false); const messagesEndRef = useRef(null); // ─── Detail loader / refresh chain ────────────────────────────── const loadTicketDetail = 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); } }; useEffect(() => { if (selectedTicketId) { loadTicketDetail(selectedTicketId); } }, [selectedTicketId]); // Auto-scroll messages list to the latest reply useEffect(() => { if (selectedTicket && messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [selectedTicket]); // ─── Mutations ────────────────────────────────────────────────── const handleTicketReply = async () => { if (!selectedTicketId || !replyText.trim()) return; setReplySending(true); try { await adminApi.replyToTicket(selectedTicketId, replyText); setReplyText(''); await loadTicketDetail(selectedTicketId); await ticketsQuery.refetch(); } catch (error) { console.error('Failed to reply:', error); } finally { setReplySending(false); } }; const handleTicketStatusChange = async (newStatus: TicketStatus) => { if (!selectedTicketId) return; setActionLoading(true); try { await adminApi.updateTicketStatus(selectedTicketId, newStatus); await loadTicketDetail(selectedTicketId); await ticketsQuery.refetch(); } catch (error) { console.error('Failed to update ticket status:', error); } finally { setActionLoading(false); } }; // ─── Render ───────────────────────────────────────────────────── return (
{selectedTicketId ? ( /* Ticket Chat View */ ticketDetailLoading ? (
) : selectedTicket ? ( { setSelectedTicketId(null); setSelectedTicket(null); }} onStatusChange={handleTicketStatusChange} onReplyTextChange={setReplyText} onReply={handleTicketReply} formatDate={formatDate} /> ) : null ) : ticketsLoading ? (
) : tickets.length === 0 ? ( ) : ( )}
); } // ────────────────────────────────────────────────────────────────── // Sub-views (kept private to this file for now) // ────────────────────────────────────────────────────────────────── function EmptyState() { const { t } = useTranslation(); return (

{t('admin.users.detail.noTickets')}

); } function TicketsList({ tickets, ticketsTotal, onOpenTicket, formatDate, }: { tickets: AdminTicket[]; ticketsTotal: number; onOpenTicket: (id: number) => void; formatDate: (date: string | null) => string; }) { const { t } = useTranslation(); const statusStyles: Record = { open: 'bg-accent-500/20 text-accent-400 border-accent-500/30', pending: 'bg-warning-500/20 text-warning-400 border-warning-500/30', answered: 'bg-success-500/20 text-success-400 border-success-500/30', closed: 'bg-dark-600 text-dark-400 border-dark-500', }; return ( <>
{ticketsTotal} {t('admin.users.detail.ticketsCount')}
{tickets.map((ticket) => ( ))}
); } function ChatView({ selectedTicket, actionLoading, replyText, replySending, messagesEndRef, onBack, onStatusChange, onReplyTextChange, onReply, formatDate, }: { selectedTicket: AdminTicketDetail; actionLoading: boolean; replyText: string; replySending: boolean; messagesEndRef: React.RefObject; onBack: () => void; onStatusChange: (s: TicketStatus) => void; onReplyTextChange: (text: string) => void; onReply: () => void; formatDate: (date: string | null) => string; }) { const { t } = useTranslation(); return (
{/* Chat header */}
#{selectedTicket.id} {selectedTicket.title}
{selectedTicket.status} {formatDate(selectedTicket.created_at)}
{/* Status buttons — 36px mobile / 26px desktop. Active state matters and gets mis-tapped on mobile when too small. */}
{STATUS_VALUES.map((s) => ( ))}
{/* Messages */}
{selectedTicket.messages.map((msg) => (
{msg.is_from_admin ? t('admin.tickets.adminLabel') : t('admin.tickets.userLabel')} {formatDate(msg.created_at)}
{msg.message_text && (

)}

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