"use client" import { type KeyboardEvent, useCallback, useMemo, useState } from "react" import { KeyRoundIcon, MoreHorizontalIcon, PlusIcon, SearchIcon, ShieldIcon, UserRoundIcon, } from "lucide-react" import { toast } from "sonner" import { DashboardPage, DashboardTableShell, DashboardTableStateRow, DashboardToolbar, } from "@/components/dashboard-page" import { useDashboardPagedList, type DashboardListFilter, } from "@/components/dashboard/list" import { ListPagination } from "@/components/list-pagination" import { assignUserRoles, createUser, fetchRoleListAll, fetchUserDetail, fetchUsers, resetUserPassword, updateUser, updateUserStatus, type AdminRole, type AdminUser, type CreateAdminUserPayload, type ResetPasswordResult, type UpdateAdminUserPayload, } from "@/lib/api/admin" import { Status } from "@/lib/generated/enums" import { useAppLocale, useI18n } from "@/i18n/provider" import { getRoleDisplayName } from "@/lib/role-i18n" import { formatDateTime } from "@/lib/utils" import { AssignRolesDrawer } from "./_components/assign-roles" import { CreateUserDrawer } from "./_components/create" import { EditDrawer } from "./_components/edit" import { InitialPasswordDialog } from "./_components/initial-password-dialog" import { ResetPasswordDialogs } from "./_components/reset-password" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { ButtonGroup } from "@/components/ui/button-group" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" export default function DashboardUsersPage() { const t = useI18n() const { locale } = useAppLocale() const [creatingOpen, setCreatingOpen] = useState(false) const [savingCreate, setSavingCreate] = useState(false) const [initialPassword, setInitialPassword] = useState<{ username: string password: string } | null>(null) const [savingEdit, setSavingEdit] = useState(false) const [savingPassword, setSavingPassword] = useState(false) const [savingRoles, setSavingRoles] = useState(false) const [editingUser, setEditingUser] = useState(null) const [resettingUser, setResettingUser] = useState(null) const [assigningRolesUser, setAssigningRolesUser] = useState(null) const [assignRoleOptions, setAssignRoleOptions] = useState([]) const [assignRoleIds, setAssignRoleIds] = useState([]) const [assignRolesLoading, setAssignRolesLoading] = useState(false) const [resetPasswordResult, setResetPasswordResult] = useState(null) const [actionLoadingId, setActionLoadingId] = useState(null) const filters = useMemo( () => [ { name: "username", label: t("user.filterUsername"), defaultValue: "", trim: true, }, ], [t], ) const fetchList = useCallback( (query: Record) => fetchUsers({ username: typeof query.username === "string" ? query.username : undefined, page: Number(query.page), limit: Number(query.limit), }), [], ) const list = useDashboardPagedList({ filters, fetchList, loadFailed: t("user.loadFailed"), }) function applyFilters() { list.applyFilters() } function handleFilterKeyDown(event: KeyboardEvent) { if (event.key !== "Enter") { return } event.preventDefault() applyFilters() } function openEditDrawer(user: AdminUser) { setEditingUser(user) } async function openAssignRolesDrawer(user: AdminUser) { setActionLoadingId(user.id) setAssigningRolesUser(user) setAssignRolesLoading(true) try { const [roles, userDetail] = await Promise.all([ fetchRoleListAll(), fetchUserDetail(user.id), ]) setAssignRoleOptions(roles) setAssignRoleIds((userDetail.roles || []).map((role) => role.id)) } catch (error) { setAssigningRolesUser(null) toast.error(error instanceof Error ? error.message : t("user.loadRoleAssignFailed")) } finally { setAssignRolesLoading(false) setActionLoadingId(null) } } function handlePageChange(nextPage: number) { list.handlePageChange(nextPage) } function handleLimitChange(nextLimit: number) { list.handleLimitChange(nextLimit) } function handleEditDrawerOpenChange(open: boolean) { if (savingEdit) { return } if (!open) { setEditingUser(null) } } function handleCreateDrawerOpenChange(open: boolean) { if (savingCreate) { return } if (!open) { setCreatingOpen(false) } } async function handleCreateUser(payload: CreateAdminUserPayload) { if (savingCreate) { return } setSavingCreate(true) try { const result = await createUser(payload) toast.success(t("user.created", { username: result.user.username })) setCreatingOpen(false) setInitialPassword({ username: result.user.username, password: result.password, }) await list.loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("user.createFailed")) } finally { setSavingCreate(false) } } function handleAssignRolesOpenChange(open: boolean) { if (savingRoles) { return } if (!open) { setAssigningRolesUser(null) setAssignRoleOptions([]) setAssignRoleIds([]) } } async function handleSaveUser(payload: UpdateAdminUserPayload) { if (savingEdit) { return } setSavingEdit(true) try { await updateUser(payload) toast.success(t("user.updated", { username: editingUser?.username || t("user.fallbackUser") })) setEditingUser(null) await list.loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("user.updateFailed")) } finally { setSavingEdit(false) } } async function handleAssignRoles(roleIds: number[]) { if (!assigningRolesUser || savingRoles) { return } setSavingRoles(true) try { await assignUserRoles(assigningRolesUser.id, roleIds) toast.success(t("user.rolesUpdated", { username: assigningRolesUser.username })) setAssigningRolesUser(null) setAssignRoleOptions([]) setAssignRoleIds([]) await list.loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("user.saveRolesFailed")) } finally { setSavingRoles(false) } } function openResetDrawer(user: AdminUser) { setResetPasswordResult(null) setResettingUser(user) } function handleResetDrawerOpenChange(open: boolean) { if (savingPassword) { return } if (!open) { setResetPasswordResult(null) setResettingUser(null) } } async function handleResetPassword() { if (!resettingUser || savingPassword) { return } setSavingPassword(true) try { const result = await resetUserPassword(resettingUser.id) setResetPasswordResult(result) toast.success(t("user.passwordReset", { username: resettingUser.username })) } catch (error) { toast.error(error instanceof Error ? error.message : t("user.resetPasswordFailed")) } finally { setSavingPassword(false) } } async function handleToggleStatus(user: AdminUser) { setActionLoadingId(user.id) try { const nextStatus = user.status === Status.Ok ? Status.Disabled : Status.Ok await updateUserStatus(user.id, nextStatus) toast.success( t("user.statusUpdated", { username: user.username, status: nextStatus === Status.Ok ? t("user.enabled") : t("user.disabled"), }) ) await list.loadData() } catch (error) { toast.error(error instanceof Error ? error.message : t("user.statusUpdateFailed")) } finally { setActionLoadingId(null) } } return ( <> setCreatingOpen(true)} disabled={list.loading}> {t("user.addUser")} } >
list.setDraftFilter("username", event.target.value) } onKeyDown={handleFilterKeyDown} placeholder={t("user.filterUsername")} className="pl-9" />
} > {t("user.columnUser")} {t("user.columnRoles")} {t("user.columnStatus")} {t("user.columnLastLogin")} {t("user.columnContact")} {t("user.columnActions")} {list.result.results.map((item) => (
{item.nickname || item.username}
{item.username}
{(item.roles || []).length > 0 ? ( item.roles?.map((role) => ( {getRoleDisplayName(role.code, role.name, locale)} )) ) : ( {t("user.unassigned")} )}
{item.status === Status.Ok ? t("user.enabled") : t("user.disabled")} {item.isSystem ? ( {t("user.system")} ) : null}
{formatDateTime(item.lastLoginAt)}
{item.lastLoginIp || "-"}
{item.mobile || "-"}
{item.email || "-"}
} aria-label={t("user.moreActions", { username: item.username })} > void openAssignRolesDrawer(item)} disabled={actionLoadingId === item.id} > {actionLoadingId === item.id ? t("user.processing") : t("user.assignRoles")} openResetDrawer(item)} disabled={actionLoadingId === item.id} > {t("user.resetPassword")} handleToggleStatus(item)} disabled={actionLoadingId === item.id} > {actionLoadingId === item.id ? t("user.processing") : item.status === Status.Ok ? t("user.disabled") : t("user.enabled")}
))} {list.loading || list.result.results.length === 0 ? ( ) : null}
{ if (!open) { setInitialPassword(null) } }} /> ) }