Files
vidconf/frontend/src/components/admin/AdminUserCreateDialog.tsx

149 lines
5.9 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 { 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>
)
}