gendesign/tradein-mvp/frontend/src/components/team/CreateEmployeeForm.tsx
bot-backend f112a4affb
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m9s
feat(tradein/ui): team-дашборд менеджера — сотрудники, квоты, история (#2556)
Единая страница /team для ролей admin/manager (backend сам скоупит список
по org-изоляции) — таблица сотрудников с пагинацией, создание сотрудника
с ручным паролем, изменение месячной квоты + сброс пароля одним PATCH,
drawer с историей оценок. Nav-пункт «Команда» в Topbar виден только
admin/manager (доп. roleGate поверх isPathAllowed — legacy analyst-роль
иначе тоже прошла бы path-фильтр).
2026-07-30 22:02:41 +03:00

219 lines
7.4 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.

"use client";
/**
* Модалка создания сотрудника (#2556, эпик #2549) — `POST /api/v1/team/employees`.
*
* Пароль задаётся вручную менеджером/админом — email-рассылки нет (см. issue
* DoD), поэтому после успешного создания показываем явное «передайте пароль
* сотруднику» вместо тихого закрытия модалки.
*/
import { useState } from "react";
import type { FormEvent } from "react";
import {
USERNAME_HINT,
USERNAME_PATTERN,
teamErrorMessage,
useCreateEmployee,
} from "@/lib/team-api";
interface CreateEmployeeFormProps {
onClose: () => void;
}
export function CreateEmployeeForm({ onClose }: CreateEmployeeFormProps) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [displayName, setDisplayName] = useState("");
const [orgName, setOrgName] = useState("");
const [email, setEmail] = useState("");
const [monthlyLimit, setMonthlyLimit] = useState("");
const [usernameTouched, setUsernameTouched] = useState(false);
const createMutation = useCreateEmployee();
const usernameValid = USERNAME_PATTERN.test(username);
const usernameInvalid = usernameTouched && username.length > 0 && !usernameValid;
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setUsernameTouched(true);
if (createMutation.isPending) return;
if (!USERNAME_PATTERN.test(username)) return;
if (password.length === 0) return;
const parsedLimit = monthlyLimit.trim() === "" ? undefined : Number(monthlyLimit);
createMutation.mutate({
username: username.trim(),
password,
display_name: displayName.trim() || undefined,
org_name: orgName.trim() || undefined,
email: email.trim() || undefined,
monthly_limit:
parsedLimit !== undefined && Number.isFinite(parsedLimit) && parsedLimit >= 1
? Math.trunc(parsedLimit)
: undefined,
});
}
if (createMutation.isSuccess) {
return (
<div className="team-modal-backdrop" role="presentation" onClick={onClose}>
<div
className="team-modal"
role="dialog"
aria-modal="true"
aria-label="Сотрудник создан"
onClick={(e) => e.stopPropagation()}
>
<h2>Сотрудник создан</h2>
<p className="team-form-success">
Логин «{createMutation.data.username}» готов. Передайте пароль сотруднику
он не сохраняется в системе и не отправляется автоматически.
</p>
<div className="team-modal-actions">
<button type="button" className="team-btn-primary" onClick={onClose}>
Готово
</button>
</div>
</div>
</div>
);
}
return (
<div className="team-modal-backdrop" role="presentation" onClick={onClose}>
<form
className="team-modal"
role="dialog"
aria-modal="true"
aria-label="Новый сотрудник"
onClick={(e) => e.stopPropagation()}
onSubmit={handleSubmit}
>
<h2>Новый сотрудник</h2>
<p className="team-modal-subtitle">
Пароль задаётся вручную передайте его сотруднику лично, рассылки нет.
</p>
{createMutation.isError ? (
<p role="alert" className="team-form-error">
{teamErrorMessage(createMutation.error)}
</p>
) : null}
<div className="team-field">
<label htmlFor="team-new-username">Логин</label>
<input
id="team-new-username"
name="username"
type="text"
autoComplete="off"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
onBlur={() => setUsernameTouched(true)}
disabled={createMutation.isPending}
aria-invalid={usernameInvalid}
/>
<p className="team-field-hint">{USERNAME_HINT}</p>
{usernameInvalid ? (
<p className="team-field-hint" style={{ color: "var(--danger, #b3261e)" }}>
Логин не соответствует формату
</p>
) : null}
</div>
<div className="team-field team-password-row">
<label htmlFor="team-new-password">Пароль</label>
<input
id="team-new-password"
name="password"
type={showPassword ? "text" : "password"}
autoComplete="new-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={createMutation.isPending}
/>
<button
type="button"
className="team-password-toggle"
onClick={() => setShowPassword((v) => !v)}
tabIndex={-1}
>
{showPassword ? "Скрыть" : "Показать"}
</button>
</div>
<div className="team-field">
<label htmlFor="team-new-display-name">Имя (необязательно)</label>
<input
id="team-new-display-name"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
disabled={createMutation.isPending}
/>
</div>
<div className="team-field">
<label htmlFor="team-new-org">Организация (необязательно)</label>
<input
id="team-new-org"
type="text"
value={orgName}
onChange={(e) => setOrgName(e.target.value)}
disabled={createMutation.isPending}
/>
</div>
<div className="team-field">
<label htmlFor="team-new-email">Email (необязательно)</label>
<input
id="team-new-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={createMutation.isPending}
/>
</div>
<div className="team-field">
<label htmlFor="team-new-limit">Месячный лимит оценок (необязательно)</label>
<input
id="team-new-limit"
type="number"
min={1}
step={1}
placeholder="по умолчанию"
value={monthlyLimit}
onChange={(e) => setMonthlyLimit(e.target.value)}
disabled={createMutation.isPending}
/>
</div>
<div className="team-modal-actions">
<button
type="button"
className="team-btn-secondary"
onClick={onClose}
disabled={createMutation.isPending}
>
Отмена
</button>
<button
type="submit"
className="team-btn-primary"
disabled={createMutation.isPending || password.length === 0 || !usernameValid}
>
{createMutation.isPending ? "Создаём…" : "Создать"}
</button>
</div>
</form>
</div>
);
}