164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
import { KeyRound, Pencil } from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
import { ApiError } from "../../api/client";
|
|
import { Spinner } from "../../components/Spinner";
|
|
import { useToast } from "../../context/ToastContext";
|
|
import { useAdminSetPassword, useAdminUpdateUser, useAdminUsers } from "../../hooks/admin";
|
|
// DEV-ТОЛЬКО: удаление аккаунтов. Импорт используется лишь под import.meta.env.DEV,
|
|
// поэтому в прод-сборке вырезается тришейкингом (как и dev-вход).
|
|
import { DevDeleteAccountButton } from "./DevDeleteAccountButton";
|
|
|
|
export function AdminAccountsPage() {
|
|
const { data: users, isLoading } = useAdminUsers();
|
|
const update = useAdminUpdateUser();
|
|
const setPassword = useAdminSetPassword();
|
|
const toast = useToast();
|
|
const [editId, setEditId] = useState<number | null>(null);
|
|
const [editName, setEditName] = useState("");
|
|
// Новый пароль игроку (восстановление забытого): отдельный режим строки, как переименование.
|
|
const [passwordId, setPasswordId] = useState<number | null>(null);
|
|
const [newPassword, setNewPassword] = useState("");
|
|
|
|
if (isLoading) return <Spinner />;
|
|
|
|
const startEdit = (id: number, nickname: string) => {
|
|
setPasswordId(null);
|
|
setEditId(id);
|
|
setEditName(nickname);
|
|
};
|
|
|
|
const startPassword = (id: number) => {
|
|
setEditId(null);
|
|
setPasswordId(id);
|
|
setNewPassword("");
|
|
};
|
|
|
|
const savePassword = async () => {
|
|
if (passwordId == null || !newPassword) return;
|
|
try {
|
|
await setPassword.mutateAsync({ userId: passwordId, password: newPassword });
|
|
toast.show("Пароль задан — сообщите его игроку");
|
|
setPasswordId(null);
|
|
setNewPassword("");
|
|
} catch (e) {
|
|
toast.error(e instanceof ApiError ? e.message : "Не удалось задать пароль");
|
|
}
|
|
};
|
|
|
|
const saveName = async () => {
|
|
const name = editName.trim();
|
|
if (editId == null || !name) return;
|
|
try {
|
|
await update.mutateAsync({ userId: editId, nickname: name });
|
|
setEditId(null);
|
|
} catch (e) {
|
|
toast.error(e instanceof ApiError ? e.message : "Не удалось переименовать");
|
|
}
|
|
};
|
|
|
|
const toggleActive = async (id: number, isActive: boolean) => {
|
|
try {
|
|
await update.mutateAsync({ userId: id, is_active: !isActive });
|
|
} catch (e) {
|
|
toast.error(e instanceof ApiError ? e.message : "Не удалось сохранить");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
{/* Название раздела уже в активной вкладке админки. */}
|
|
<div className="stack">
|
|
{(users ?? []).map((u) => (
|
|
<div key={u.id} className="card" style={{ margin: 0 }}>
|
|
{editId === u.id ? (
|
|
// Режим переименования.
|
|
<div className="row">
|
|
<input
|
|
value={editName}
|
|
autoFocus
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") saveName();
|
|
if (e.key === "Escape") setEditId(null);
|
|
}}
|
|
/>
|
|
<button
|
|
className="btn btn-primary small"
|
|
disabled={update.isPending || !editName.trim() || editName.trim() === u.nickname}
|
|
onClick={saveName}
|
|
>
|
|
Сохранить
|
|
</button>
|
|
<button className="btn btn-ghost small" onClick={() => setEditId(null)}>
|
|
Отмена
|
|
</button>
|
|
</div>
|
|
) : passwordId === u.id ? (
|
|
// Режим «задать пароль».
|
|
<div className="row">
|
|
<input
|
|
type="password"
|
|
autoComplete="new-password"
|
|
placeholder={`Новый пароль для ${u.nickname}`}
|
|
value={newPassword}
|
|
autoFocus
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") savePassword();
|
|
if (e.key === "Escape") setPasswordId(null);
|
|
}}
|
|
/>
|
|
<button
|
|
className="btn btn-primary small"
|
|
disabled={setPassword.isPending || !newPassword}
|
|
onClick={savePassword}
|
|
>
|
|
Задать
|
|
</button>
|
|
<button className="btn btn-ghost small" onClick={() => setPasswordId(null)}>
|
|
Отмена
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="row-between">
|
|
<div>
|
|
<b>{u.nickname}</b> {u.role === "admin" && <span className="badge">админ</span>}
|
|
<div className="muted small">
|
|
#{u.id} · {u.auth_provider} · {u.is_active ? "активен" : "отключён"}
|
|
{u.role !== "admin" && !u.has_password && " · без пароля"}
|
|
</div>
|
|
</div>
|
|
{u.role !== "admin" && (
|
|
<div className="row" style={{ gap: 4 }}>
|
|
<button
|
|
className="btn btn-ghost small"
|
|
title="Переименовать"
|
|
onClick={() => startEdit(u.id, u.nickname)}
|
|
>
|
|
<Pencil size={16} />
|
|
</button>
|
|
<button
|
|
className="btn btn-ghost small"
|
|
title="Задать пароль"
|
|
onClick={() => startPassword(u.id)}
|
|
>
|
|
<KeyRound size={16} />
|
|
</button>
|
|
{import.meta.env.DEV && (
|
|
<DevDeleteAccountButton userId={u.id} nickname={u.nickname} />
|
|
)}
|
|
<button className="btn small" onClick={() => toggleActive(u.id, u.is_active)}>
|
|
{u.is_active ? "Отключить" : "Включить"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|