Files
EventHubFrontAdmin/src/pages/admins/AdminDetailPage.tsx
T

314 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useForm, Controller } from 'react-hook-form';
import dayjs from 'dayjs';
import { ArrowLeft, Pencil } from 'lucide-react';
import { useAdmin, useUpdateAdmin } from '@/hooks/useAdmins';
import { formatDisplayValue } from '@/lib/utils';
import { PageHeader } from '@/components/PageHeader';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
const isBadValue = (val: unknown) =>
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
const clean = (val: unknown) => (val === '-' || val === 'undefined' ? undefined : val);
const roleBadgeVariant = (role: string) => {
if (role === 'superadmin') return 'destructive' as const;
if (role === 'admin') return 'default' as const;
if (role === 'moderator') return 'secondary' as const;
return 'outline' as const;
};
interface AdminFormValues {
nickname?: string;
email?: string;
role?: string;
status?: string;
timezone?: string;
language?: string;
phone?: string;
}
const DetailValue: React.FC<{ value: unknown; emptyLabel: string }> = ({ value, emptyLabel }) => {
const text = formatDisplayValue(value);
if (text === '-') return <>{emptyLabel}</>;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return <pre className="overflow-auto rounded-md bg-muted p-2 text-xs">{text}</pre>;
}
return <>{text}</>;
};
const AdminDetailPage: React.FC = () => {
const { t } = useTranslation();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { data: admin, isLoading } = useAdmin(id || '');
const updateAdmin = useUpdateAdmin();
const [editModal, setEditModal] = useState(false);
const form = useForm<AdminFormValues>();
const emDash = t('common.emDash');
const formatDate = (dateStr: string) => {
if (isBadValue(dateStr)) return emDash;
const d = dayjs(dateStr);
return d.isValid() ? d.format('DD.MM.YYYY HH:mm') : dateStr;
};
useEffect(() => {
if (editModal && admin) {
form.reset({
nickname: clean(admin.nickname) as string | undefined,
email: clean(admin.email) as string | undefined,
role: clean(admin.role) as string | undefined,
status: clean(admin.status) as string | undefined,
timezone: clean(admin.timezone) as string | undefined,
language: clean(admin.language) as string | undefined,
phone: clean(admin.phone) as string | undefined,
});
}
}, [editModal, admin, form]);
const onSave = (values: AdminFormValues) => {
const payload = Object.fromEntries(
Object.entries(values).filter(([_, v]) => v !== '' && v !== undefined && v !== null)
);
updateAdmin.mutate(
{ id: id!, data: payload },
{
onSuccess: () => {
setEditModal(false);
},
}
);
};
if (isLoading) {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-96 w-full" />
</div>
);
}
if (!admin) return <p className="text-muted-foreground">{t('admins.notFound')}</p>;
const titleName = !isBadValue(admin.nickname) ? admin.nickname : admin.email || admin.id;
return (
<div data-testid="page-admin-detail">
<PageHeader
title={t('admins.detailTitle', { name: titleName })}
breadcrumbs={[
{ label: t('admins.title'), href: '/admins' },
{ label: String(titleName) },
]}
actions={
<>
<Button
variant="outline"
size="sm"
data-testid="btn-edit-admin"
onClick={() => setEditModal(true)}
>
<Pencil className="h-4 w-4" />
{t('common.edit')}
</Button>
<Button variant="outline" data-testid="btn-back" onClick={() => navigate('/admins')}>
<ArrowLeft className="h-4 w-4" />
{t('common.backToList')}
</Button>
</>
}
/>
<Card>
<CardContent className="space-y-6">
<dl className="grid gap-3 text-sm">
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.id')}</dt>
<dd>{admin.id}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.email')}</dt>
<dd>{admin.email}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.nickname')}</dt>
<dd>{isBadValue(admin.nickname) ? emDash : admin.nickname}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.role')}</dt>
<dd>
<Badge variant={roleBadgeVariant(admin.role)}>{admin.role}</Badge>
</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.status')}</dt>
<dd>
<Badge variant={admin.status === 'active' ? 'success' : 'destructive'}>{admin.status}</Badge>
</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.timezone')}</dt>
<dd>{isBadValue(admin.timezone) ? emDash : admin.timezone}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.language')}</dt>
<dd>{isBadValue(admin.language) ? emDash : admin.language}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.phone')}</dt>
<dd>{isBadValue(admin.phone) ? emDash : admin.phone}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('profile.avatarUrl')}</dt>
<dd>{isBadValue(admin.avatar_url) ? emDash : admin.avatar_url}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('profile.preferencesJson')}</dt>
<dd><DetailValue value={admin.preferences} emptyLabel={emDash} /></dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.lastLogin')}</dt>
<dd>{formatDate(admin.last_login)}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.createdAt')}</dt>
<dd>{formatDate(admin.created_at)}</dd>
</div>
<div className="grid grid-cols-[140px_1fr] gap-2">
<dt className="text-muted-foreground">{t('common.updatedAt')}</dt>
<dd>{formatDate(admin.updated_at)}</dd>
</div>
</dl>
</CardContent>
</Card>
<Dialog open={editModal} onOpenChange={setEditModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('admins.editTitle')}</DialogTitle>
</DialogHeader>
<form id="admin-edit-form" className="space-y-4" onSubmit={form.handleSubmit(onSave)}>
<div className="space-y-2">
<Label htmlFor="nickname">{t('common.nickname')}</Label>
<Input id="nickname" {...form.register('nickname')} />
</div>
<div className="space-y-2">
<Label htmlFor="email">{t('common.email')}</Label>
<Input id="email" {...form.register('email')} />
</div>
<div className="space-y-2">
<Label>{t('common.role')}</Label>
<Controller
name="role"
control={form.control}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="superadmin">superadmin</SelectItem>
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="moderator">moderator</SelectItem>
<SelectItem value="support">support</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div className="space-y-2">
<Label>{t('common.status')}</Label>
<Controller
name="status"
control={form.control}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="active">active</SelectItem>
<SelectItem value="blocked">blocked</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div className="space-y-2">
<Label>{t('common.timezone')}</Label>
<Controller
name="timezone"
control={form.control}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger><SelectValue placeholder={t('common.selectTimezone')} /></SelectTrigger>
<SelectContent>
<SelectItem value="UTC">UTC</SelectItem>
<SelectItem value="Europe/Moscow">Europe/Moscow (Москва)</SelectItem>
<SelectItem value="Europe/London">Europe/London (Лондон)</SelectItem>
<SelectItem value="Europe/Berlin">Europe/Berlin (Берлин)</SelectItem>
<SelectItem value="America/New_York">America/New_York (Нью-Йорк)</SelectItem>
<SelectItem value="Asia/Tokyo">Asia/Tokyo (Токио)</SelectItem>
<SelectItem value="Asia/Shanghai">Asia/Shanghai (Шанхай)</SelectItem>
<SelectItem value="Australia/Sydney">Australia/Sydney (Сидней)</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div className="space-y-2">
<Label>{t('common.language')}</Label>
<Controller
name="language"
control={form.control}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger><SelectValue placeholder={t('common.selectLanguage')} /></SelectTrigger>
<SelectContent>
<SelectItem value="ru">{t('common.langRu')}</SelectItem>
<SelectItem value="en">{t('common.langEn')}</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone">{t('common.phone')}</Label>
<Input id="phone" placeholder="+7 (999) 123-45-67" {...form.register('phone')} />
</div>
</form>
<DialogFooter>
<Button variant="outline" onClick={() => setEditModal(false)}>{t('common.cancel')}</Button>
<Button type="submit" form="admin-edit-form" disabled={updateAdmin.isPending}>
{updateAdmin.isPending ? t('common.saving') : t('common.save')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default AdminDetailPage;