import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ChevronLeft, ChevronRight, FileDown, Pencil, Plus, RefreshCw, Search, Settings2, Trash2, User, } from "lucide-react"; import * as React from "react"; import { Link, useNavigate } from "react-router-dom"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "../../components/ui/dialog"; import { Input } from "../../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import { toast } from "../../components/ui/use-toast"; import { bulkDeleteUsers, bulkUpdateUsers, deleteUser, exportUsersCSVUrl, fetchMe, fetchTenant, fetchTenants, fetchUsers, } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { UserBulkMoveGroupModal } from "./components/UserBulkMoveGroupModal"; import { UserBulkUploadModal } from "./components/UserBulkUploadModal"; type UserSchemaField = { key: string; label: string; type: string; }; function UserListPage() { const navigate = useNavigate(); const [page, setPage] = React.useState(1); const [search, setSearch] = React.useState(""); const [searchDraft, setSearchDraft] = React.useState(""); const [selectedCompany, setSelectedCompany] = React.useState(""); const [visibleColumns, setVisibleColumns] = React.useState< Record >({}); const [selectedUserIds, setSelectedUserIds] = React.useState([]); const limit = 50; const offset = (page - 1) * limit; const { data: profile } = useQuery({ queryKey: ["me"], queryFn: fetchMe, }); const { data: tenantsData } = useQuery({ queryKey: ["tenants", { limit: 100 }], queryFn: () => fetchTenants(100, 0), }); const tenants = tenantsData?.items ?? []; // Lock company for tenant_admin React.useEffect(() => { if (profile?.role === "tenant_admin" && profile.tenantSlug) { setSelectedCompany(profile.tenantSlug); } }, [profile]); const selectedTenantId = React.useMemo(() => { return tenants.find((t) => t.slug === selectedCompany)?.id ?? ""; }, [tenants, selectedCompany]); const { data: tenantDetail } = useQuery({ queryKey: ["tenant", selectedTenantId], queryFn: () => fetchTenant(selectedTenantId), enabled: selectedTenantId.length > 0, }); const userSchema: UserSchemaField[] = Array.isArray( tenantDetail?.config?.userSchema, ) ? (tenantDetail?.config?.userSchema as UserSchemaField[]) : []; // Initialize visible columns when schema changes React.useEffect(() => { if (userSchema.length > 0) { const initial: Record = {}; for (const field of userSchema) { initial[field.key] = true; } setVisibleColumns((prev) => { // Only set if not already set for these keys to avoid reset on every render const next = { ...initial, ...prev }; return next; }); } }, [userSchema]); const toggleColumn = (key: string) => { setVisibleColumns((prev) => ({ ...prev, [key]: !prev[key], })); }; const query = useQuery({ queryKey: ["users", { limit, offset, search, tenantSlug: selectedCompany }], queryFn: () => fetchUsers(limit, offset, search, selectedCompany), placeholderData: (previousData) => previousData, }); const deleteMutation = useMutation({ mutationFn: (userId: string) => deleteUser(userId), onSuccess: () => { query.refetch(); }, }); const handleSearch = () => { setSearch(searchDraft); setPage(1); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { handleSearch(); } }; const handleExport = () => { const url = exportUsersCSVUrl(search, selectedCompany); window.open(url, "_blank"); }; const errorMsg = (query.error as AxiosError<{ error?: string }>)?.response ?.data?.error; const fallbackError = !errorMsg && query.isError ? t( "msg.admin.users.list.fetch_error", "사용자 목록 조회에 실패했습니다.", ) : null; const items = query.data?.items ?? []; const total = query.data?.total ?? 0; const totalPages = Math.ceil(total / limit); const toggleSelectAll = () => { if (selectedUserIds.length === items.length) { setSelectedUserIds([]); } else { setSelectedUserIds(items.map((u) => u.id)); } }; const toggleSelectUser = (id: string) => { setSelectedUserIds((prev) => prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id], ); }; const bulkDeleteMutation = useMutation({ mutationFn: bulkDeleteUsers, onSuccess: () => { query.refetch(); setSelectedUserIds([]); toast.success( t( "msg.admin.users.bulk.delete_success", "선택한 사용자들이 삭제되었습니다.", ), ); }, }); const bulkUpdateMutation = useMutation({ mutationFn: bulkUpdateUsers, onSuccess: () => { query.refetch(); setSelectedUserIds([]); toast.success( t( "msg.admin.users.bulk.update_success", "선택한 사용자들의 정보가 수정되었습니다.", ), ); }, }); const handleBulkStatusChange = (status: string) => { if (selectedUserIds.length === 0) return; bulkUpdateMutation.mutate({ userIds: selectedUserIds, status }); }; const handleBulkDelete = () => { if (selectedUserIds.length === 0) return; if ( window.confirm( t( "msg.admin.users.bulk.delete_confirm", "{{count}}명의 사용자를 정말 삭제하시겠습니까?", { count: selectedUserIds.length }, ), ) ) { bulkDeleteMutation.mutate(selectedUserIds); } }; const handleDelete = (userId: string, userName: string) => { if ( !window.confirm( t( "msg.admin.users.list.delete_confirm", '사용자 "{{name}}"을(를) 정말 삭제하시겠습니까?', { name: userName }, ), ) ) { return; } deleteMutation.mutate(userId); }; return (

{t("ui.admin.users.list.title", "사용자 관리")}

{t( "msg.admin.users.list.subtitle", "시스템 사용자를 조회하고 관리합니다.", )}

query.refetch()} /> {t("ui.admin.users.list.columns.title", "표시 컬럼 설정")} {t( "msg.admin.users.list.columns.description", "사용자 목록에 표시할 커스텀 필드를 선택하세요.", )}
{userSchema.length === 0 && (

{t( "msg.admin.users.list.columns.no_custom", "현재 테넌트에 정의된 커스텀 필드가 없습니다.", )}

)} {userSchema.map((field) => ( ))}
{t("ui.admin.users.list.registry.title", "User Registry")} {t( "msg.admin.users.list.registry.count", "총 {{count}}명의 사용자가 등록되어 있습니다.", { count: total }, )}
setSearchDraft(e.target.value)} onKeyDown={handleKeyDown} />
{t("ui.admin.users.list.filter.tenant", "테넌트 필터:")}
{(errorMsg || fallbackError) && (
{errorMsg ?? fallbackError}
)}
0 && selectedUserIds.length === items.length } onChange={toggleSelectAll} /> {t( "ui.admin.users.list.table.name_email", "NAME / EMAIL", )} {t("ui.admin.users.list.table.login_id", "LOGIN ID")} {t("ui.admin.users.list.table.role", "ROLE")} {t("ui.admin.users.list.table.status", "STATUS")} {t( "ui.admin.users.list.table.tenant_dept", "TENANT / DEPT", )} {/* Dynamic Columns from Schema */} {userSchema.map( (field) => visibleColumns[field.key] !== false && ( {field.label} ), )} {t("ui.admin.users.list.table.created", "CREATED")} {t("ui.admin.users.list.table.actions", "ACTIONS")} {query.isLoading && ( {t("msg.common.loading", "로딩 중...")} )} {!query.isLoading && items.length === 0 && ( {t( "msg.admin.users.list.empty", "검색 결과가 없습니다.", )} )} {items.map((user) => ( toggleSelectUser(user.id)} />
{user.name} {user.email}
{user.loginId || "-"} {t(`ui.admin.role.${user.role}`, user.role)} {t(`ui.common.status.${user.status}`, user.status)}
{user.tenant?.name || user.tenantSlug || "-"} {user.department || "-"}
{/* Dynamic Metadata Cells */} {userSchema.map( (field) => visibleColumns[field.key] !== false && ( {String(user.metadata?.[field.key] ?? "-")} ), )} {new Date(user.createdAt).toLocaleDateString()}
))}
{/* Bulk Action Bar */} {selectedUserIds.length > 0 && (
{t("ui.admin.users.bulk.selected_count", "{{count}}명 선택됨", { count: selectedUserIds.length, })}
selectedUserIds.includes(u.id), )} onSuccess={() => { query.refetch(); setSelectedUserIds([]); }} />
)} {/* Pagination */} {totalPages > 1 && (
{t("ui.common.page_of", "Page {{page}} of {{total}}", { page, total: totalPages, })}
)}
); } export default UserListPage;