mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
Merge pull request #169 from BEDOLAGA-DEV/feat/user-tickets-tab
feat: tickets tab in admin user detail
This commit is contained in:
@@ -101,6 +101,7 @@ export const adminApi = {
|
||||
per_page?: number;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
user_id?: number;
|
||||
} = {},
|
||||
): Promise<AdminTicketListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tickets', { params });
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1542,8 +1542,12 @@
|
||||
"info": "اطلاعات",
|
||||
"subscription": "اشتراک",
|
||||
"balance": "موجودی",
|
||||
"sync": "همگامسازی"
|
||||
"sync": "همگامسازی",
|
||||
"tickets": "تیکتها"
|
||||
},
|
||||
"noTickets": "این کاربر تیکتی ندارد",
|
||||
"ticketsCount": "تیکت",
|
||||
"messagesCount": "پیام",
|
||||
"status": "وضعیت",
|
||||
"language": "زبان",
|
||||
"registration": "ثبتنام",
|
||||
|
||||
@@ -2366,8 +2366,12 @@
|
||||
"info": "Информация",
|
||||
"subscription": "Подписка",
|
||||
"balance": "Баланс",
|
||||
"sync": "Синхронизация"
|
||||
"sync": "Синхронизация",
|
||||
"tickets": "Тикеты"
|
||||
},
|
||||
"noTickets": "У пользователя нет тикетов",
|
||||
"ticketsCount": "тикетов",
|
||||
"messagesCount": "сообщений",
|
||||
"status": "Статус",
|
||||
"language": "Язык",
|
||||
"registration": "Регистрация",
|
||||
|
||||
@@ -1541,8 +1541,12 @@
|
||||
"info": "信息",
|
||||
"subscription": "订阅",
|
||||
"balance": "余额",
|
||||
"sync": "同步"
|
||||
"sync": "同步",
|
||||
"tickets": "工单"
|
||||
},
|
||||
"noTickets": "该用户没有工单",
|
||||
"ticketsCount": "个工单",
|
||||
"messagesCount": "条消息",
|
||||
"status": "状态",
|
||||
"language": "语言",
|
||||
"registration": "注册时间",
|
||||
|
||||
@@ -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<UserDetailResponse | null>(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<PanelSyncStatusResponse | null>(null);
|
||||
const [tariffs, setTariffs] = useState<UserAvailableTariff[]>([]);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
@@ -102,6 +105,11 @@ export default function AdminUserDetail() {
|
||||
const [balanceAmount, setBalanceAmount] = useState<number | ''>('');
|
||||
const [balanceDescription, setBalanceDescription] = useState('');
|
||||
|
||||
// Tickets
|
||||
const [tickets, setTickets] = useState<AdminTicket[]>([]);
|
||||
const [ticketsLoading, setTicketsLoading] = useState(false);
|
||||
const [ticketsTotal, setTicketsTotal] = useState(0);
|
||||
|
||||
// Subscription form
|
||||
const [subAction, setSubAction] = useState<string>('extend');
|
||||
const [subDays, setSubDays] = useState<number | ''>(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) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
@@ -421,6 +444,7 @@ export default function AdminUserDetail() {
|
||||
{tab === 'subscription' && t('admin.users.detail.tabs.subscription')}
|
||||
{tab === 'balance' && t('admin.users.detail.tabs.balance')}
|
||||
{tab === 'sync' && t('admin.users.detail.tabs.sync')}
|
||||
{tab === 'tickets' && t('admin.users.detail.tabs.tickets')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1009,6 +1033,80 @@ export default function AdminUserDetail() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tickets Tab */}
|
||||
{activeTab === 'tickets' && (
|
||||
<div className="space-y-4">
|
||||
{ticketsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
||||
</div>
|
||||
) : tickets.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl bg-dark-800/50 py-12">
|
||||
<svg
|
||||
className="mb-3 h-12 w-12 text-dark-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-dark-400">{t('admin.users.detail.noTickets')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm text-dark-400">
|
||||
{ticketsTotal} {t('admin.users.detail.ticketsCount')}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{tickets.map((ticket) => {
|
||||
const statusStyles: Record<string, string> = {
|
||||
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 (
|
||||
<button
|
||||
key={ticket.id}
|
||||
onClick={() => navigate(`/admin/tickets?ticket=${ticket.id}`)}
|
||||
className="w-full rounded-xl bg-dark-800/50 p-4 text-left transition-colors hover:bg-dark-700/50"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium text-dark-100">
|
||||
#{ticket.id} {ticket.title}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full border px-2 py-0.5 text-xs ${statusStyles[ticket.status] || statusStyles.closed}`}
|
||||
>
|
||||
{ticket.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-dark-500">
|
||||
<span>{formatDate(ticket.created_at)}</span>
|
||||
<span>
|
||||
{ticket.messages_count} {t('admin.users.detail.messagesCount')}
|
||||
</span>
|
||||
</div>
|
||||
{ticket.last_message && (
|
||||
<div className="mt-2 truncate text-sm text-dark-400">
|
||||
{ticket.last_message.is_from_admin ? '> ' : ''}
|
||||
{ticket.last_message.message_text}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user