From 995c0348dc8a65bc3e8432911c15137fe7e72bfa Mon Sep 17 00:00:00 2001 From: Fringg Date: Sat, 7 Feb 2026 05:21:16 +0300 Subject: [PATCH] feat: add tickets tab to admin user detail page Add ability to view user's tickets directly from the user detail page. Backend gets user_id filter on tickets endpoint, frontend shows ticket list with status badges, message preview and navigation to ticket. --- src/api/admin.ts | 1 + src/locales/en.json | 6 +- src/locales/fa.json | 6 +- src/locales/ru.json | 6 +- src/locales/zh.json | 6 +- src/pages/AdminUserDetail.tsx | 104 +++++++++++++++++++++++++++++++++- 6 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/api/admin.ts b/src/api/admin.ts index 1d5faa4..843f969 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -101,6 +101,7 @@ export const adminApi = { per_page?: number; status?: string; priority?: string; + user_id?: number; } = {}, ): Promise => { const response = await apiClient.get('/cabinet/admin/tickets', { params }); diff --git a/src/locales/en.json b/src/locales/en.json index 4ca39d0..7aa8a25 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1839,8 +1839,12 @@ "info": "Information", "subscription": "Subscription", "balance": "Balance", - "sync": "Synchronization" + "sync": "Synchronization", + "tickets": "Tickets" }, + "noTickets": "No tickets from this user", + "ticketsCount": "tickets", + "messagesCount": "messages", "status": "Status", "language": "Language", "registration": "Registration", diff --git a/src/locales/fa.json b/src/locales/fa.json index 6dc458d..29cc70f 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1542,8 +1542,12 @@ "info": "اطلاعات", "subscription": "اشتراک", "balance": "موجودی", - "sync": "همگام‌سازی" + "sync": "همگام‌سازی", + "tickets": "تیکت‌ها" }, + "noTickets": "این کاربر تیکتی ندارد", + "ticketsCount": "تیکت", + "messagesCount": "پیام", "status": "وضعیت", "language": "زبان", "registration": "ثبت‌نام", diff --git a/src/locales/ru.json b/src/locales/ru.json index c7faef0..866b1c7 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2366,8 +2366,12 @@ "info": "Информация", "subscription": "Подписка", "balance": "Баланс", - "sync": "Синхронизация" + "sync": "Синхронизация", + "tickets": "Тикеты" }, + "noTickets": "У пользователя нет тикетов", + "ticketsCount": "тикетов", + "messagesCount": "сообщений", "status": "Статус", "language": "Язык", "registration": "Регистрация", diff --git a/src/locales/zh.json b/src/locales/zh.json index b6435e6..92486fb 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1541,8 +1541,12 @@ "info": "信息", "subscription": "订阅", "balance": "余额", - "sync": "同步" + "sync": "同步", + "tickets": "工单" }, + "noTickets": "该用户没有工单", + "ticketsCount": "个工单", + "messagesCount": "条消息", "status": "状态", "language": "语言", "registration": "注册时间", diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 5a0c6c0..8a40ddc 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -11,6 +11,7 @@ import { type PanelSyncStatusResponse, type UpdateSubscriptionRequest, } from '../api/adminUsers'; +import { adminApi, type AdminTicket } from '../api/admin'; import { AdminBackButton } from '../components/admin'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; @@ -89,7 +90,9 @@ export default function AdminUserDetail() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState<'info' | 'subscription' | 'balance' | 'sync'>('info'); + const [activeTab, setActiveTab] = useState< + 'info' | 'subscription' | 'balance' | 'sync' | 'tickets' + >('info'); const [syncStatus, setSyncStatus] = useState(null); const [tariffs, setTariffs] = useState([]); const [actionLoading, setActionLoading] = useState(false); @@ -102,6 +105,11 @@ export default function AdminUserDetail() { const [balanceAmount, setBalanceAmount] = useState(''); const [balanceDescription, setBalanceDescription] = useState(''); + // Tickets + const [tickets, setTickets] = useState([]); + const [ticketsLoading, setTicketsLoading] = useState(false); + const [ticketsTotal, setTicketsTotal] = useState(0); + // Subscription form const [subAction, setSubAction] = useState('extend'); const [subDays, setSubDays] = useState(30); @@ -143,6 +151,20 @@ export default function AdminUserDetail() { } }, [userId]); + const loadTickets = useCallback(async () => { + if (!userId) return; + try { + setTicketsLoading(true); + const data = await adminApi.getTickets({ user_id: userId, per_page: 50 }); + setTickets(data.items); + setTicketsTotal(data.total); + } catch (error) { + console.error('Failed to load tickets:', error); + } finally { + setTicketsLoading(false); + } + }, [userId]); + useEffect(() => { if (!userId || isNaN(userId)) { navigate('/admin/users'); @@ -154,7 +176,8 @@ export default function AdminUserDetail() { useEffect(() => { if (activeTab === 'sync') loadSyncStatus(); if (activeTab === 'subscription') loadTariffs(); - }, [activeTab, loadSyncStatus, loadTariffs]); + if (activeTab === 'tickets') loadTickets(); + }, [activeTab, loadSyncStatus, loadTariffs, loadTickets]); const handleUpdateBalance = async (isAdd: boolean) => { if (balanceAmount === '' || !userId) return; @@ -407,7 +430,7 @@ export default function AdminUserDetail() { className="scrollbar-hide -mx-4 mb-6 flex gap-2 overflow-x-auto px-4 py-1" style={{ WebkitOverflowScrolling: 'touch' }} > - {(['info', 'subscription', 'balance', 'sync'] as const).map((tab) => ( + {(['info', 'subscription', 'balance', 'sync', 'tickets'] as const).map((tab) => ( ))} @@ -1009,6 +1033,80 @@ export default function AdminUserDetail() { )} + + {/* Tickets Tab */} + {activeTab === 'tickets' && ( +
+ {ticketsLoading ? ( +
+
+
+ ) : tickets.length === 0 ? ( +
+ + + +

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

+
+ ) : ( + <> +
+ {ticketsTotal} {t('admin.users.detail.ticketsCount')} +
+
+ {tickets.map((ticket) => { + const statusStyles: Record = { + open: 'bg-blue-500/20 text-blue-400 border-blue-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 ( + + ); + })} +
+ + )} +
+ )}
);