"use client"; import { KeyRound, Pencil, Trash2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useExportLabels } from "@/hooks/use-export-labels"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { deleteAdminUser, getAdminUserPermissionCatalog, getAdminUsers, postAdminUser, putAdminUser, putAdminUserRoles, } from "@/api/admin-users"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { Badge } from "@/components/ui/badge"; import { resolveAdminUserStatusTone } from "@/lib/admin-status-tone"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { PRD_ADMIN_USER_MANAGE } from "@/lib/admin-prd"; import { cn } from "@/lib/utils"; import { useAdminProfile } from "@/stores/admin-session"; import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index"; import { LotteryApiBizError } from "@/types/api/errors"; export function AdminUsersConsole(): React.ReactElement { const { t } = useTranslation(["adminUsers", "common"]); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); const exportLabels = useExportLabels("adminUsers"); const profile = useAdminProfile(); const canManageUsers = adminHasAnyPermission(profile?.permissions, [PRD_ADMIN_USER_MANAGE]); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(10); const [keyword, setKeyword] = useState(""); const [query, setQuery] = useState(""); const [catalog, setCatalog] = useState(null); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [lastPage, setLastPage] = useState(1); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [selectedId, setSelectedId] = useState(null); const [draftRoles, setDraftRoles] = useState([]); const [savingRoles, setSavingRoles] = useState(false); const [permissionOpen, setPermissionOpen] = useState(false); const [accountOpen, setAccountOpen] = useState(false); const [accountMode, setAccountMode] = useState<"create" | "edit">("create"); const [accountSaving, setAccountSaving] = useState(false); const [editingAccountId, setEditingAccountId] = useState(null); const [formUsername, setFormUsername] = useState(""); const [formNickname, setFormNickname] = useState(""); const [formEmail, setFormEmail] = useState(""); const [formPassword, setFormPassword] = useState(""); const [formStatus, setFormStatus] = useState(0); const [formCreateRoles, setFormCreateRoles] = useState([]); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteBusy, setDeleteBusy] = useState(false); const selectedUser = useMemo( () => items.find((u) => u.id === selectedId) ?? null, [items, selectedId], ); const roleNameBySlug = useMemo( () => new Map((catalog?.roles ?? []).map((role) => [role.slug, role.name])), [catalog], ); const load = useCallback(async () => { setLoading(true); setErr(null); try { const [catalogData, listData] = await Promise.all([ getAdminUserPermissionCatalog(), getAdminUsers({ page, per_page: perPage, keyword: query.trim() || undefined, }), ]); setCatalog(catalogData); setItems(listData.items); setTotal(listData.meta.total); setLastPage(Math.max(1, listData.meta.last_page)); } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : t("loadFailed"); setErr(msg); setItems([]); setTotal(0); setLastPage(1); } finally { setLoading(false); } }, [page, perPage, query, t]); useEffect(() => { queueMicrotask(() => { void load(); }); }, [load]); function toggleFormCreateRole(slug: string, checked: boolean): void { setFormCreateRoles((prev) => { if (checked) { return Array.from(new Set([...prev, slug])).sort(); } return prev.filter((value) => value !== slug); }); } function toggleRole(slug: string, checked: boolean): void { setDraftRoles((prev) => { if (checked) { return Array.from(new Set([...prev, slug])).sort(); } return prev.filter((value) => value !== slug); }); } function openPermissionEditor(row: AdminUserPermissionRow): void { setSelectedId(row.id); setDraftRoles([...row.roles].sort()); setPermissionOpen(true); } function handlePermissionDialogOpenChange(open: boolean): void { setPermissionOpen(open); if (!open) { setSelectedId(null); } } function openCreateAccount(): void { setAccountMode("create"); setEditingAccountId(null); setFormUsername(""); setFormNickname(""); setFormEmail(""); setFormPassword(""); setFormStatus(0); setFormCreateRoles([]); setAccountOpen(true); } function openEditAccount(row: AdminUserPermissionRow): void { setAccountMode("edit"); setEditingAccountId(row.id); setFormUsername(row.username); setFormNickname(row.nickname); setFormEmail(row.email ?? ""); setFormPassword(""); setFormStatus(row.status); setAccountOpen(true); } function handleAccountDialogOpenChange(open: boolean): void { setAccountOpen(open); if (!open) { setEditingAccountId(null); } } async function submitAccount(): Promise { const nickname = formNickname.trim(); if (nickname === "") { toast.error(t("nicknameRequired")); return; } if (accountMode === "edit" && formPassword !== "" && formPassword.length < 8) { toast.error(t("newPasswordMin")); return; } if (accountMode === "create" && formCreateRoles.length === 0) { toast.error(t("roleRequired")); return; } setAccountSaving(true); try { if (accountMode === "create") { const username = formUsername.trim(); if (username === "") { toast.error(t("usernameRequired")); return; } if (formPassword.length < 8) { toast.error(t("passwordMin")); return; } const created = await postAdminUser({ username: username.toLowerCase(), nickname, email: formEmail.trim() === "" ? null : formEmail.trim(), password: formPassword, status: formStatus, role_slugs: formCreateRoles, }); setItems((prev) => [created, ...prev]); setTotal((prev) => prev + 1); toast.success(t("createSuccess", { name: created.username })); handleAccountDialogOpenChange(false); } else { const id = editingAccountId; if (id === null) { return; } const body: { nickname: string; email: string | null; status: number; password?: string; } = { nickname, email: formEmail.trim() === "" ? null : formEmail.trim(), status: formStatus, }; if (formPassword.trim() !== "") { body.password = formPassword.trim(); } const updated = await putAdminUser(id, body); setItems((prev) => prev.map((row) => (row.id === updated.id ? updated : row))); toast.success(t("updateSuccess", { name: updated.username })); handleAccountDialogOpenChange(false); } } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : t("saveAccountFailed"); toast.error(msg); } finally { setAccountSaving(false); } } async function saveRoles(): Promise { if (!selectedUser) { return; } setSavingRoles(true); try { const result = await putAdminUserRoles(selectedUser.id, draftRoles); setDraftRoles([...result.roles].sort()); setItems((prev) => prev.map((row) => row.id === result.id ? { ...row, roles: result.roles, effective_permissions: result.effective_permissions, } : row, ), ); toast.success(t("saveRoleSuccess", { name: result.username })); } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : t("saveRoleFailed"); toast.error(msg); } finally { setSavingRoles(false); } } async function confirmDelete(): Promise { if (!deleteTarget) { return; } setDeleteBusy(true); try { await deleteAdminUser(deleteTarget.id); setItems((prev) => prev.filter((row) => row.id !== deleteTarget.id)); setTotal((prev) => Math.max(0, prev - 1)); toast.success(t("deleteSuccess", { name: deleteTarget.username })); setDeleteTarget(null); } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : t("deleteFailed"); toast.error(msg); } finally { setDeleteBusy(false); } } return (
{t("listTitle")} {canManageUsers ? ( ) : null}
setKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { setPage(1); setQuery(keyword.trim()); } }} />
{err ?

{err}

: null} {loading && items.length === 0 ? (

{t("states.loading", { ns: "common" })}

) : null}
{t("table.id", { ns: "common" })} {t("table.account")} {t("table.nickname")} {t("table.status")} {t("table.roles")} {t("table.effective")} {t("table.actions")} {items.length === 0 ? ( {t("states.noData", { ns: "common" })} ) : ( items.map((row) => ( {row.id}
{row.username} {row.email ?? ""}
{row.nickname ?? ""} {row.status === 0 ? t("status.enabled") : t("status.disabled")}
{row.roles.length === 0 ? ( {t("common.none")} ) : ( row.roles.map((slug) => ( {roleNameBySlug.get(slug) ?? slug} )) )}
{row.effective_permissions.length} {canManageUsers ? ( openPermissionEditor(row), }, { key: "edit", label: t("actions.edit"), icon: Pencil, onClick: () => openEditAccount(row), }, { key: "delete", label: t("actions.delete"), icon: Trash2, destructive: true, disabled: profile?.id === row.id, onClick: () => setDeleteTarget(row), }, ]} /> ) : ( )}
)) )}
{ setPerPage(value); setPage(1); }} onPageChange={setPage} />
{t("permissionDialog.title")} {selectedUser ? ( <> {selectedUser.username} · {selectedUser.nickname} ) : null}

{t("permissionDialog.rolesDescription")}

{(catalog?.roles ?? []).map((role) => { const checked = draftRoles.includes(role.slug); return ( ); })}
{accountMode === "create" ? t("accountDialog.createTitle") : t("accountDialog.editTitle")} {accountMode === "create" ? t("accountDialog.createDescription") : t("accountDialog.editDescription")}
{t("accountDialog.username")}
setFormUsername(e.target.value)} />
{t("accountDialog.nickname")}
setFormNickname(e.target.value)} />
{t("accountDialog.emailOptional")}
setFormEmail(e.target.value)} />
{accountMode === "edit" ? t("accountDialog.passwordOptional") : t("accountDialog.password")}
setFormPassword(e.target.value)} />
{accountMode === "create" ? (
{t("accountDialog.rolesRequired")}

{t("accountDialog.rolesDescription")}

{(catalog?.roles ?? []).length === 0 ? (

{t("accountDialog.noRoles")}

) : ( (catalog?.roles ?? []).map((role) => { const checked = formCreateRoles.includes(role.slug); return ( ); }) )}
) : null}

{t("table.status")}

{formStatus === 0 ? t("status.enabled") : t("status.disabled")}

setFormStatus(checked ? 0 : 1)} />
!open && setDeleteTarget(null)}> {t("delete.confirmTitle")} {deleteTarget ? t("delete.confirmDescription", { name: deleteTarget.username }) : null}
); }