fix and update

This commit is contained in:
lendry
2026-07-01 23:27:48 +03:00
parent 2b88e028c6
commit bcdfbc3861
36 changed files with 1504 additions and 320 deletions

View File

@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Ban, BadgeCheck, Crown, FileText, KeyRound, Loader2, ScrollText, Search, ShieldOff, ShieldPlus, UserCheck, UserCog } from 'lucide-react';
import { Ban, BadgeCheck, Crown, FileText, KeyRound, Loader2, MoreVertical, ScrollText, Search, ShieldOff, ShieldPlus, UserCheck, UserCog } from 'lucide-react';
import { UserInspectorDialog } from '@/components/admin/user-inspector-dialog';
import { VerificationBadge } from '@/components/id/verification-badge';
import { UserDocumentsDialog } from '@/components/documents/user-documents-dialog';
@@ -10,10 +10,16 @@ import { AdminShell } from '@/components/id/admin-shell';
import { useAuth } from '@/components/id/auth-provider';
import { useToast } from '@/components/id/toast-provider';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { AdminPermission, AdminRole, AdminUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus, getApiErrorMessage } from '@/lib/api';
import { AdminPermission, AdminRole, AdminUser, PublicUser, adminDisableUserTotp, apiFetch, fetchUserTotpStatus, getApiErrorMessage } from '@/lib/api';
import { getAdminLandingPath } from '@/lib/admin-access';
import { DEFAULT_VERIFICATION_ICON, VERIFICATION_ICON_OPTIONS } from '@/lib/verification-icons';
@@ -34,6 +40,113 @@ const roleLabels: Record<string, string> = {
const DEFAULT_USER_ROLE = 'user';
function UserActionsMenu({
user,
currentUser,
actionLoading,
onOpenInspector,
onResetPassword,
onSuspend,
onUnsuspend,
onOpenDocuments,
onDisableTotp,
onOpenVerification,
onOpenRoles,
onToggleSuperAdmin
}: {
user: AdminUser;
currentUser: PublicUser | null;
actionLoading: boolean;
onOpenInspector: () => void;
onResetPassword: () => void;
onSuspend: () => void;
onUnsuspend: () => void;
onOpenDocuments: () => void;
onDisableTotp: () => void;
onOpenVerification: () => void;
onOpenRoles: () => void;
onToggleSuperAdmin: () => void;
}) {
const canInspect = (currentUser?.canViewUsers || currentUser?.canManageUsers) && !user.isBot;
const canManage = Boolean(currentUser?.canManageUsers);
const canDocuments = Boolean(currentUser?.canViewUserDocuments && !user.isBot);
const canDisableTotp = Boolean(currentUser?.isSuperAdmin && !user.isBot);
const canVerify = Boolean(currentUser?.canVerifyUsers);
const canRoles = Boolean(currentUser?.canManageRoles);
if (!canInspect && !canManage && !canDocuments && !canDisableTotp && !canVerify && !canRoles) {
return null;
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" aria-label="Действия с пользователем" disabled={actionLoading}>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 rounded-xl">
{canInspect ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenInspector}>
<ScrollText className="h-4 w-4" />
Журнал и чаты
</DropdownMenuItem>
) : null}
{canManage ? (
<>
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onResetPassword}>
<KeyRound className="h-4 w-4" />
Сбросить пароль
</DropdownMenuItem>
<DropdownMenuItem className="gap-2 rounded-lg" disabled={user.status === 'SUSPENDED'} onClick={onSuspend}>
<Ban className="h-4 w-4" />
Заблокировать
</DropdownMenuItem>
<DropdownMenuItem className="gap-2 rounded-lg" disabled={user.status !== 'SUSPENDED'} onClick={onUnsuspend}>
<UserCheck className="h-4 w-4" />
Разблокировать
</DropdownMenuItem>
</>
) : null}
{canDocuments ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenDocuments}>
<FileText className="h-4 w-4" />
Документы
</DropdownMenuItem>
) : null}
{canDisableTotp ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onDisableTotp}>
<ShieldOff className="h-4 w-4" />
Отключить 2FA
</DropdownMenuItem>
) : null}
{canVerify ? (
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenVerification}>
<BadgeCheck className="h-4 w-4" />
{user.isVerified ? 'Изменить верификацию' : 'Верифицировать'}
</DropdownMenuItem>
) : null}
{canRoles ? (
<>
<DropdownMenuItem className="gap-2 rounded-lg" onClick={onOpenRoles}>
<UserCog className="h-4 w-4" />
Роли и доступ
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2 rounded-lg"
disabled={user.id === currentUser?.id}
onClick={onToggleSuperAdmin}
>
<Crown className="h-4 w-4" />
{user.isSuperAdmin ? 'Снять супер-админа' : 'Назначить супер-админом'}
</DropdownMenuItem>
</>
) : null}
</DropdownMenuContent>
</DropdownMenu>
);
}
export default function AdminUsersPage() {
const router = useRouter();
const { token, user: currentUser } = useAuth();
@@ -367,98 +480,28 @@ export default function AdminUsersPage() {
<span className="rounded-full bg-[#f4f5f8] px-3 py-1 text-xs font-semibold">{statusLabels[user.status] ?? user.status}</span>
</TableCell>
<TableCell>
<div className="flex justify-end gap-2" onClick={(event) => event.stopPropagation()}>
{(currentUser?.canViewUsers || currentUser?.canManageUsers) && !user.isBot ? (
<Button
variant="secondary"
size="icon"
aria-label="Журнал и чаты"
onClick={() => setInspectorUser(user)}
>
<ScrollText className="h-4 w-4" />
</Button>
) : null}
{currentUser?.canManageUsers ? (
<>
<Button
variant="secondary"
size="icon"
aria-label="Сбросить пароль"
disabled={actionLoading}
onClick={() => {
setSelectedUser(user);
setPassword('');
setDialog('password');
}}
>
<KeyRound className="h-4 w-4" />
</Button>
<Button variant="secondary" size="icon" aria-label="Заблокировать" disabled={actionLoading || user.status === 'SUSPENDED'} onClick={() => void handleSuspend(user)}>
<Ban className="h-4 w-4" />
</Button>
<Button variant="secondary" size="icon" aria-label="Разблокировать" disabled={actionLoading || user.status !== 'SUSPENDED'} onClick={() => void handleUnsuspend(user)}>
<UserCheck className="h-4 w-4" />
</Button>
</>
) : null}
{currentUser?.canViewUserDocuments && !user.isBot ? (
<Button
variant="secondary"
size="icon"
aria-label="Документы пользователя"
disabled={actionLoading}
onClick={() => setDocumentsUser(user)}
>
<FileText className="h-4 w-4" />
</Button>
) : null}
{currentUser?.isSuperAdmin && !user.isBot ? (
<Button
variant="secondary"
size="icon"
aria-label="Отключить 2FA"
disabled={actionLoading}
onClick={() => void handleAdminDisableTotp(user)}
>
<ShieldOff 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
variant="secondary"
size="icon"
aria-label="Управление ролями"
disabled={actionLoading}
onClick={() => {
setSelectedUser(user);
setDialog('roles');
}}
>
<UserCog className="h-4 w-4" />
</Button>
<Button
variant={user.isSuperAdmin ? 'default' : 'secondary'}
size="icon"
aria-label="Супер-админ"
disabled={actionLoading || user.id === currentUser?.id}
onClick={() => void handleToggleSuperAdmin(user)}
>
<Crown className="h-4 w-4" />
</Button>
</>
) : null}
<div className="flex justify-end" onClick={(event) => event.stopPropagation()}>
<UserActionsMenu
user={user}
currentUser={currentUser}
actionLoading={actionLoading}
onOpenInspector={() => setInspectorUser(user)}
onResetPassword={() => {
setSelectedUser(user);
setPassword('');
setDialog('password');
}}
onSuspend={() => void handleSuspend(user)}
onUnsuspend={() => void handleUnsuspend(user)}
onOpenDocuments={() => setDocumentsUser(user)}
onDisableTotp={() => void handleAdminDisableTotp(user)}
onOpenVerification={() => openVerificationDialog(user)}
onOpenRoles={() => {
setSelectedUser(user);
setDialog('roles');
}}
onToggleSuperAdmin={() => void handleToggleSuperAdmin(user)}
/>
</div>
</TableCell>
</TableRow>

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Car, ChevronRight, FileKey2, FileText, Loader2, Mail, Phone, Trash2, UserRound } from 'lucide-react';
import { AddressQuickSection } from '@/components/addresses/address-quick-section';
import { DocumentFormDialog } from '@/components/documents/document-form-dialog';
import { DocumentDialog } from '@/components/documents/document-dialog';
import { DocumentPhotosInline } from '@/components/documents/document-photo-gallery';
import { ActionTile } from '@/components/id/action-tile';
import { useAuth } from '@/components/id/auth-provider';
@@ -20,6 +20,7 @@ import { useRequireAuth } from '@/hooks/use-require-auth';
import {
getDocumentType,
indexDocumentsByType,
parseAttachmentMeta,
parseDocumentPhotos,
parseMetadata,
type DocumentTypeCode
@@ -324,6 +325,7 @@ export default function DataPage() {
documents.map((document) => {
const config = getDocumentType(document.type);
const photoKeys = parseDocumentPhotos(parseMetadata(document.metadataJson));
const attachmentMeta = parseAttachmentMeta(parseMetadata(document.metadataJson));
return (
<div key={document.id}>
<Row
@@ -334,7 +336,7 @@ export default function DataPage() {
/>
{photoKeys.length > 0 && userId && token ? (
<div className="border-b border-[#eceef4] px-1 pb-4">
<DocumentPhotosInline userId={userId} token={token} storageKeys={photoKeys} />
<DocumentPhotosInline userId={userId} token={token} storageKeys={photoKeys} attachmentMeta={attachmentMeta} />
</div>
) : null}
</div>
@@ -401,7 +403,7 @@ export default function DataPage() {
</section>
{activeDocumentType && user ? (
<DocumentFormDialog
<DocumentDialog
open={Boolean(activeDocumentType)}
onOpenChange={(open) => {
if (!open) setActiveDocumentType(null);

View File

@@ -3,7 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { ChevronRight, Plus } from 'lucide-react';
import { DocumentFormDialog } from '@/components/documents/document-form-dialog';
import { DocumentDialog } from '@/components/documents/document-dialog';
import { DocumentPhotosInline } from '@/components/documents/document-photo-gallery';
import { useAuth } from '@/components/id/auth-provider';
import { IdShell } from '@/components/id/shell';
@@ -12,6 +12,7 @@ import {
DOCUMENT_CATEGORIES,
DOCUMENT_TYPES,
getDocumentType,
parseAttachmentMeta,
parseDocumentPhotos,
parseMetadata,
QUICK_DOCUMENT_TYPES,
@@ -97,6 +98,7 @@ export default function DocumentsPage() {
const Icon = item.icon;
const existing = documentsByType.get(item.type);
const photoKeys = existing ? parseDocumentPhotos(parseMetadata(existing.metadataJson)) : [];
const attachmentMeta = existing ? parseAttachmentMeta(parseMetadata(existing.metadataJson)) : {};
return (
<div key={item.type} className="border-b border-white/70 last:border-b-0">
<button
@@ -115,7 +117,7 @@ export default function DocumentsPage() {
</button>
{existing && photoKeys.length > 0 && user && token ? (
<div className="px-4 pb-4">
<DocumentPhotosInline userId={user.id} token={token} storageKeys={photoKeys} />
<DocumentPhotosInline userId={user.id} token={token} storageKeys={photoKeys} attachmentMeta={attachmentMeta} />
</div>
) : null}
</div>
@@ -126,7 +128,7 @@ export default function DocumentsPage() {
))}
{activeType ? (
<DocumentFormDialog
<DocumentDialog
open={Boolean(activeType)}
onOpenChange={(open) => {
if (!open) setActiveType(null);

View File

@@ -15,6 +15,29 @@
* {
box-sizing: border-box;
scrollbar-width: thin;
scrollbar-color: rgb(168 173 188 / 55%) transparent;
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background: rgb(168 173 188 / 45%);
border-radius: 999px;
border: 1px solid transparent;
background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover {
background: rgb(102 112 133 / 65%);
background-clip: padding-box;
}
body {
@@ -29,6 +52,10 @@ html {
overflow-x: hidden;
}
*::-webkit-scrollbar-corner {
background: transparent;
}
.rdp-root {
--rdp-accent-color: #111827;
--rdp-accent-background-color: #f4f5f8;

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { BadgeCheck, Bell, Car, FileText, Fingerprint, Heart, KeyRound, PawPrint, ShieldCheck, Smartphone, UserRound } from 'lucide-react';
import { AddressQuickSection } from '@/components/addresses/address-quick-section';
import { DocumentFormDialog } from '@/components/documents/document-form-dialog';
import { DocumentDialog } from '@/components/documents/document-dialog';
import { ActionTile, SectionTitle } from '@/components/id/action-tile';
import { AdminBadge } from '@/components/id/admin-badge';
import { AvatarDisplay } from '@/components/id/avatar-upload';
@@ -124,7 +124,7 @@ export default function HomePage() {
</div>
{activeDocumentType && user ? (
<DocumentFormDialog
<DocumentDialog
open={Boolean(activeDocumentType)}
onOpenChange={(open) => {
if (!open) setActiveDocumentType(null);