fix and update
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user