'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Activity, CheckCircle2, Loader2, MessageSquare, Search, ShieldAlert, Trash2, XCircle } from 'lucide-react'; import { VerificationBadge } from '@/components/id/verification-badge'; import { useToast } from '@/components/id/toast-provider'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { AdminChatMessage, AdminChatRoomSummary, AdminUser, AdminUserActivityItem, SignInEvent, apiFetch } from '@/lib/api'; import { cn } from '@/lib/utils'; function formatDateTime(value: string) { return new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(value)); } function useDebouncedValue(value: T, delayMs = 350) { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = window.setTimeout(() => setDebounced(value), delayMs); return () => window.clearTimeout(timer); }, [delayMs, value]); return debounced; } const roomTypeLabels: Record = { GENERAL: 'Общий', DIRECT: 'Личный', GROUP: 'Группа' }; export function UserInspectorDialog({ user, token, canModerateChats, open, onOpenChange }: { user: AdminUser | null; token: string | null; canModerateChats: boolean; open: boolean; onOpenChange: (open: boolean) => void; }) { const { showToast } = useToast(); const [tab, setTab] = useState('sign-in'); const [signInSearch, setSignInSearch] = useState(''); const [activitySearch, setActivitySearch] = useState(''); const [chatSearch, setChatSearch] = useState(''); const [messageSearch, setMessageSearch] = useState(''); const [globalMessageSearch, setGlobalMessageSearch] = useState(''); const debouncedSignInSearch = useDebouncedValue(signInSearch); const debouncedActivitySearch = useDebouncedValue(activitySearch); const debouncedChatSearch = useDebouncedValue(chatSearch); const debouncedMessageSearch = useDebouncedValue(messageSearch); const debouncedGlobalMessageSearch = useDebouncedValue(globalMessageSearch); const [signInEvents, setSignInEvents] = useState([]); const [signInTotal, setSignInTotal] = useState(0); const [activityItems, setActivityItems] = useState([]); const [activityTotal, setActivityTotal] = useState(0); const [rooms, setRooms] = useState([]); const [roomsTotal, setRoomsTotal] = useState(0); const [selectedRoomId, setSelectedRoomId] = useState(null); const [roomMessages, setRoomMessages] = useState([]); const [roomContext, setRoomContext] = useState<{ name: string; groupName: string; type: string } | null>(null); const [searchResults, setSearchResults] = useState([]); const [searchTotal, setSearchTotal] = useState(0); const [loadingSignIn, setLoadingSignIn] = useState(false); const [loadingActivity, setLoadingActivity] = useState(false); const [loadingRooms, setLoadingRooms] = useState(false); const [loadingMessages, setLoadingMessages] = useState(false); const [loadingSearch, setLoadingSearch] = useState(false); const [deletingMessageId, setDeletingMessageId] = useState(null); const selectedRoom = useMemo(() => rooms.find((room) => room.id === selectedRoomId) ?? null, [rooms, selectedRoomId]); const loadSignIn = useCallback(async () => { if (!token || !user) return; setLoadingSignIn(true); try { const params = new URLSearchParams(); if (debouncedSignInSearch.trim()) params.set('search', debouncedSignInSearch.trim()); params.set('limit', '50'); const response = await apiFetch<{ total: number; events: SignInEvent[] }>( `/admin/users/${user.id}/sign-in-history?${params.toString()}`, {}, token ); setSignInEvents(response.events ?? []); setSignInTotal(response.total ?? 0); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось загрузить историю входов'); } finally { setLoadingSignIn(false); } }, [debouncedSignInSearch, showToast, token, user]); const loadActivity = useCallback(async () => { if (!token || !user) return; setLoadingActivity(true); try { const params = new URLSearchParams(); if (debouncedActivitySearch.trim()) params.set('search', debouncedActivitySearch.trim()); params.set('limit', '50'); const response = await apiFetch<{ total: number; items: AdminUserActivityItem[] }>( `/admin/users/${user.id}/activity?${params.toString()}`, {}, token ); setActivityItems(response.items ?? []); setActivityTotal(response.total ?? 0); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось загрузить активность'); } finally { setLoadingActivity(false); } }, [debouncedActivitySearch, showToast, token, user]); const loadRooms = useCallback(async () => { if (!token || !user || !canModerateChats) return; setLoadingRooms(true); try { const params = new URLSearchParams(); if (debouncedChatSearch.trim()) params.set('search', debouncedChatSearch.trim()); params.set('limit', '50'); const response = await apiFetch<{ total: number; rooms: AdminChatRoomSummary[] }>( `/admin/users/${user.id}/chats?${params.toString()}`, {}, token ); setRooms(response.rooms ?? []); setRoomsTotal(response.total ?? 0); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось загрузить чаты'); } finally { setLoadingRooms(false); } }, [canModerateChats, debouncedChatSearch, showToast, token, user]); const loadRoomMessages = useCallback(async () => { if (!token || !user || !selectedRoomId) return; setLoadingMessages(true); try { const params = new URLSearchParams(); if (debouncedMessageSearch.trim()) params.set('search', debouncedMessageSearch.trim()); params.set('limit', '80'); const response = await apiFetch<{ room?: { id: string; name: string; type: string; groupName: string }; messages: AdminChatMessage[]; }>(`/admin/users/${user.id}/chats/${selectedRoomId}/messages?${params.toString()}`, {}, token); setRoomMessages(response.messages ?? []); setRoomContext( response.room ? { name: response.room.name, groupName: response.room.groupName, type: response.room.type } : null ); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось загрузить сообщения'); } finally { setLoadingMessages(false); } }, [debouncedMessageSearch, selectedRoomId, showToast, token, user]); const loadGlobalSearch = useCallback(async () => { if (!token || !user || !canModerateChats || !debouncedGlobalMessageSearch.trim()) { setSearchResults([]); setSearchTotal(0); return; } setLoadingSearch(true); try { const params = new URLSearchParams(); params.set('search', debouncedGlobalMessageSearch.trim()); params.set('limit', '50'); const response = await apiFetch<{ total: number; messages: AdminChatMessage[] }>( `/admin/users/${user.id}/chats/search?${params.toString()}`, {}, token ); setSearchResults(response.messages ?? []); setSearchTotal(response.total ?? 0); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось выполнить поиск'); } finally { setLoadingSearch(false); } }, [canModerateChats, debouncedGlobalMessageSearch, showToast, token, user]); useEffect(() => { if (!open || !user) return; if (tab === 'sign-in') void loadSignIn(); }, [loadSignIn, open, tab, user]); useEffect(() => { if (!open || !user) return; if (tab === 'activity') void loadActivity(); }, [loadActivity, open, tab, user]); useEffect(() => { if (!open || !user || !canModerateChats) return; if (tab === 'chats') void loadRooms(); }, [canModerateChats, loadRooms, open, tab, user]); useEffect(() => { if (!open || !user || !selectedRoomId) return; void loadRoomMessages(); }, [loadRoomMessages, open, selectedRoomId, user]); useEffect(() => { if (!open || !user || tab !== 'chats') return; void loadGlobalSearch(); }, [loadGlobalSearch, open, tab, user]); useEffect(() => { if (!open) { setTab('sign-in'); setSignInSearch(''); setActivitySearch(''); setChatSearch(''); setMessageSearch(''); setGlobalMessageSearch(''); setSelectedRoomId(null); setRoomMessages([]); setSearchResults([]); } }, [open]); async function handleDeleteMessage(messageId: string) { if (!token || !user || !canModerateChats) return; const confirmed = window.confirm('Удалить это сообщение? Действие необратимо для пользователей чата.'); if (!confirmed) return; setDeletingMessageId(messageId); try { await apiFetch(`/admin/users/${user.id}/chat-messages/${messageId}`, { method: 'DELETE' }, token); setRoomMessages((prev) => prev.filter((item) => item.id !== messageId)); setSearchResults((prev) => prev.filter((item) => item.id !== messageId)); showToast('Сообщение удалено'); } catch (error) { showToast(error instanceof Error ? error.message : 'Не удалось удалить сообщение'); } finally { setDeletingMessageId(null); } } if (!user) return null; return ( {user.displayName} {user.isVerified ? : null}

{[user.email, user.phone, user.username].filter(Boolean).join(' · ') || 'Контакты не указаны'}

Входы Активность Чаты
setSignInSearch(event.target.value)} placeholder="Поиск по IP, устройству, user-agent, причине..." className="pl-10" />

Найдено: {signInTotal}

{loadingSignIn ? (
Загрузка...
) : signInEvents.length ? (
{signInEvents.map((event) => (
{event.success ? ( ) : ( )}
{event.success ? 'Успешный вход' : 'Неудачная попытка'} {formatDateTime(event.createdAt)}

{[event.deviceName, event.ipAddress].filter(Boolean).join(' · ') || 'Устройство не определено'}

{event.reason ?

{event.reason}

: null} {event.userAgent ? (

{event.userAgent}

) : null}
))}
) : (

События входа не найдены

)}
setActivitySearch(event.target.value)} placeholder="Поиск по действиям, документам, чатам, OAuth..." className="pl-10" />

Найдено: {activityTotal}

{loadingActivity ? (
Загрузка...
) : activityItems.length ? (
{activityItems.map((item) => (
{item.title} {item.action} {formatDateTime(item.createdAt)}
{item.detail ?

{item.detail}

: null}
))}
) : (

Активность не найдена

)}
{!canModerateChats ? (

Недостаточно прав для модерации чатов

) : ( <>
setGlobalMessageSearch(event.target.value)} placeholder="Глобальный поиск по сообщениям пользователя..." className="pl-10" />
{debouncedGlobalMessageSearch.trim() ? (

Результаты поиска по сообщениям: {searchTotal}

{loadingSearch ? (
Поиск...
) : searchResults.length ? (
{searchResults.map((message) => ( ))}
) : (

Сообщения не найдены

)}
) : null}
setChatSearch(event.target.value)} placeholder="Поиск чатов..." className="pl-10" />

Чатов: {roomsTotal}. E2E и боты скрыты.

{loadingRooms ? (
) : rooms.length ? (
{rooms.map((room) => ( ))}
) : (

Чаты не найдены

)}

{selectedRoom?.name ?? 'Выберите чат'}

{roomContext ? `${roomContext.groupName} · ${roomTypeLabels[roomContext.type] ?? roomContext.type}` : selectedRoom ? `${selectedRoom.groupName} · ${selectedRoom.memberCount} участников` : 'Обычные чаты доступны для модерации'}

setMessageSearch(event.target.value)} placeholder="Поиск в выбранном чате..." className="pl-10" disabled={!selectedRoomId} />
{!selectedRoomId ? (

Выберите чат слева

) : loadingMessages ? (
Загрузка сообщений...
) : roomMessages.length ? (
{roomMessages.map((message) => (
{message.senderName} {formatDateTime(message.createdAt)} {message.isDeleted ? ( Удалено ) : null}

{message.isEncrypted ? '[E2E недоступно для модерации]' : message.preview}

{!message.isDeleted && !message.isEncrypted ? ( ) : null}
))}
) : (

Сообщения не найдены

)}
)}
); }