Первоначальная версия VidConf
This commit is contained in:
148
frontend/src/components/admin/AdminUserCreateDialog.tsx
Normal file
148
frontend/src/components/admin/AdminUserCreateDialog.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { X } from 'lucide-react'
|
||||
import { createAdminUser, listAdminTeams } from '@/api/admin'
|
||||
import { ApiError, errorDetail } from '@/api/client'
|
||||
import { Select } from '@/components/ui/Select'
|
||||
import { useToast } from '@/components/ui/ToastProvider'
|
||||
|
||||
/** Верхний предел выборки команд для селекта — без отдельной пагинации в этом контексте (как в AdminUsersTab). */
|
||||
const TEAMS_LIMIT = 200
|
||||
|
||||
interface AdminUserCreateDialogProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Диалог «Добавить пользователя» в админке (вкладка «Пользователи») —
|
||||
* по образцу `AdminUserProfileDialog`. ФИО, email,
|
||||
* команда (справочник команд) и пароль; создание сразу с подтверждённой
|
||||
* почтой (`email_verified = true` выставляет backend). Роль остаётся
|
||||
* дефолтной («Пользователь») — сменить её можно потом из таблицы.
|
||||
*/
|
||||
export function AdminUserCreateDialog({ onClose }: AdminUserCreateDialogProps) {
|
||||
const toast = useToast()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [teamId, setTeamId] = useState('')
|
||||
const [emailError, setEmailError] = useState<string | null>(null)
|
||||
|
||||
const { data: teamsData } = useQuery({
|
||||
queryKey: ['admin', 'teams', 'all'],
|
||||
queryFn: () => listAdminTeams({ limit: TEAMS_LIMIT, offset: 0 }),
|
||||
})
|
||||
const teams = teamsData?.items ?? []
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createAdminUser({ name_user: name.trim(), email: email.trim(), password, team_id: teamId === '' ? null : teamId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
|
||||
toast.show('Пользователь создан', 'success')
|
||||
onClose()
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
if (err instanceof ApiError && err.status === 409) {
|
||||
setEmailError('Email уже зарегистрирован')
|
||||
} else if (err instanceof ApiError && err.status === 404 && errorDetail(err) === 'team_not_found') {
|
||||
toast.show('Выбранная команда не найдена — обновите список команд', 'error')
|
||||
} else {
|
||||
toast.show('Не удалось создать пользователя', 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
setEmailError(null)
|
||||
mutation.mutate()
|
||||
}
|
||||
|
||||
const canSubmit = name.trim().length > 0 && email.trim().length > 0 && password.length >= 8
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="admin-user-create-title" onClick={onClose}>
|
||||
<div className="modal-panel" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-head">
|
||||
<h2 id="admin-user-create-title">Добавить пользователя</h2>
|
||||
<button type="button" className="modal-close" aria-label="Закрыть" onClick={onClose}>
|
||||
<X className="lucide" style={{ width: 16, height: 16 }} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="field">
|
||||
<label htmlFor="admin-create-name">ФИО</label>
|
||||
<input
|
||||
id="admin-create-name"
|
||||
type="text"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
maxLength={255}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`field${emailError ? ' has-error' : ''}`}>
|
||||
<label htmlFor="admin-create-email">Email</label>
|
||||
<input
|
||||
id="admin-create-email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value)
|
||||
setEmailError(null)
|
||||
}}
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="field-hint" style={{ color: 'var(--color-danger)' }}>
|
||||
{emailError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label id="admin-create-team-label" htmlFor="admin-create-team">
|
||||
Команда
|
||||
</label>
|
||||
<Select
|
||||
id="admin-create-team"
|
||||
aria-labelledby="admin-create-team-label"
|
||||
value={teamId}
|
||||
onChange={setTeamId}
|
||||
options={[{ value: '', label: 'Без команды' }, ...teams.map((t) => ({ value: t.id, label: t.name }))]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="admin-create-password">Пароль</label>
|
||||
<input
|
||||
id="admin-create-password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={8}
|
||||
placeholder="Минимум 8 символов"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<p className="field-hint">Пользователь сможет сменить пароль после входа, в своём профиле</p>
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={!canSubmit || mutation.isPending}>
|
||||
{mutation.isPending ? 'Создаём…' : 'Создать'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user