fix and update

This commit is contained in:
lendry
2026-06-25 16:01:41 +03:00
parent ce58e6f4c1
commit 3880c68d59
40 changed files with 1715 additions and 215 deletions

View File

@@ -2,7 +2,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Ban, Crown, FileText, KeyRound, Loader2, Search, ShieldPlus, UserCheck, UserCog } from 'lucide-react';
import { Ban, BadgeCheck, Crown, FileText, KeyRound, Loader2, Search, ShieldPlus, UserCheck, UserCog } from 'lucide-react';
import { VerificationBadge } from '@/components/id/verification-badge';
import { UserDocumentsDialog } from '@/components/documents/user-documents-dialog';
import { AdminShell } from '@/components/id/admin-shell';
import { useAuth } from '@/components/id/auth-provider';
@@ -13,6 +14,7 @@ import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { AdminPermission, AdminRole, AdminUser, apiFetch } from '@/lib/api';
import { getAdminLandingPath } from '@/lib/admin-access';
import { DEFAULT_VERIFICATION_ICON, VERIFICATION_ICON_OPTIONS } from '@/lib/verification-icons';
const statusLabels: Record<string, string> = {
ACTIVE: 'Активен',
@@ -42,8 +44,9 @@ export default function AdminUsersPage() {
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null);
const [password, setPassword] = useState('');
const [actionLoading, setActionLoading] = useState(false);
const [dialog, setDialog] = useState<'password' | 'roles' | null>(null);
const [dialog, setDialog] = useState<'password' | 'roles' | 'verification' | null>(null);
const [documentsUser, setDocumentsUser] = useState<AdminUser | null>(null);
const [verificationIcon, setVerificationIcon] = useState(DEFAULT_VERIFICATION_ICON);
const loadUsers = useCallback(async () => {
if (!token) return;
@@ -224,6 +227,53 @@ export default function AdminUsersPage() {
}
}
async function handleSaveVerification() {
if (!token || !selectedUser) return;
setActionLoading(true);
try {
const updated = await apiFetch<AdminUser>(`/admin/users/${selectedUser.id}/verification`, {
method: 'PATCH',
body: JSON.stringify({
isVerified: true,
verificationIcon
})
}, token);
showToast('Пользователь верифицирован');
setSelectedUser(updated);
setDialog(null);
await loadUsers();
} catch (error) {
showToast(error instanceof Error ? error.message : 'Не удалось верифицировать пользователя');
} finally {
setActionLoading(false);
}
}
async function handleRemoveVerification() {
if (!token || !selectedUser) return;
setActionLoading(true);
try {
await apiFetch<AdminUser>(`/admin/users/${selectedUser.id}/verification`, {
method: 'PATCH',
body: JSON.stringify({ isVerified: false })
}, token);
showToast('Верификация снята');
setSelectedUser({ ...selectedUser, isVerified: false, verificationIcon: undefined });
setDialog(null);
await loadUsers();
} catch (error) {
showToast(error instanceof Error ? error.message : 'Не удалось снять верификацию');
} finally {
setActionLoading(false);
}
}
function openVerificationDialog(user: AdminUser) {
setSelectedUser(user);
setVerificationIcon(user.verificationIcon ?? DEFAULT_VERIFICATION_ICON);
setDialog('verification');
}
function renderRoles(user: AdminUser) {
const userRoles = user.roles ?? [];
const extraRoles = userRoles.filter((role) => role !== DEFAULT_USER_ROLE);
@@ -271,7 +321,10 @@ export default function AdminUsersPage() {
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>
<div className="font-medium">{user.displayName}</div>
<div className="flex items-center gap-2">
<span className="font-medium">{user.displayName}</span>
{user.isVerified ? <VerificationBadge verificationIcon={user.verificationIcon} size="xs" /> : null}
</div>
<div className="text-sm text-[#667085]">{user.email ?? user.username ?? '—'}</div>
</TableCell>
<TableCell>{user.phone ?? '—'}</TableCell>
@@ -315,6 +368,17 @@ export default function AdminUsersPage() {
<FileText className="h-4 w-4" />
</Button>
) : null}
{currentUser?.canVerifyUsers ? (
<Button
variant={user.isVerified ? 'default' : 'secondary'}
size="icon"
aria-label="Верификация"
disabled={actionLoading}
onClick={() => openVerificationDialog(user)}
>
<BadgeCheck className="h-4 w-4" />
</Button>
) : null}
{currentUser?.canManageRoles ? (
<>
<Button
@@ -432,6 +496,49 @@ export default function AdminUsersPage() {
</DialogContent>
</Dialog>
<Dialog open={dialog === 'verification'} onOpenChange={(open) => !open && setDialog(null)}>
<DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Верификация пользователя</DialogTitle>
</DialogHeader>
<p className="mb-4 text-sm text-[#667085]">{selectedUser?.displayName}</p>
{selectedUser?.isVerified ? (
<div className="mb-4 flex items-center gap-2 rounded-xl bg-[#eef4ff] px-3 py-2 text-sm">
<VerificationBadge verificationIcon={selectedUser.verificationIcon} size="sm" />
<span>Пользователь уже верифицирован</span>
</div>
) : null}
<div className="space-y-3">
<p className="text-sm font-medium">Выберите значок</p>
<div className="grid grid-cols-5 gap-2">
{VERIFICATION_ICON_OPTIONS.map((option) => (
<button
key={option.slug}
type="button"
className={`flex flex-col items-center gap-1 rounded-xl border px-2 py-3 text-xs transition ${
verificationIcon === option.slug ? 'border-[#3390ec] bg-[#eef4ff]' : 'border-[#eceef4] bg-[#fafbfd] hover:bg-[#f4f5f8]'
}`}
onClick={() => setVerificationIcon(option.slug)}
>
<option.Icon className="h-5 w-5 text-[#3390ec]" />
<span className="text-center leading-tight">{option.name}</span>
</button>
))}
</div>
</div>
<div className="mt-4 flex flex-col gap-2">
<Button className="w-full" disabled={actionLoading} onClick={() => void handleSaveVerification()}>
{actionLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : selectedUser?.isVerified ? 'Обновить значок' : 'Верифицировать'}
</Button>
{selectedUser?.isVerified ? (
<Button variant="secondary" className="w-full text-red-600" disabled={actionLoading} onClick={() => void handleRemoveVerification()}>
Снять верификацию
</Button>
) : null}
</div>
</DialogContent>
</Dialog>
{documentsUser ? (
<UserDocumentsDialog
open={Boolean(documentsUser)}