import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ChevronDown, ChevronRight, Plus, RefreshCw, Shield, Trash2, UserMinus, UserPlus, Users, } from "lucide-react"; import type React from "react"; import { useState } from "react"; import { useParams } from "react-router-dom"; import { toast } from "sonner"; import { Badge } from "../../../components/ui/badge"; import { Button } from "../../../components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../../components/ui/card"; import { Input } from "../../../components/ui/input"; import { Label } from "../../../components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../../components/ui/table"; import { type GroupSummary, addGroupMember, createGroup, deleteGroup, fetchGroups, removeGroupMember, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; type UserGroupNode = GroupSummary & { children: UserGroupNode[] }; function buildGroupTree(groups: GroupSummary[]): UserGroupNode[] { const nodeMap = new Map(); const rootNodes: UserGroupNode[] = []; groups.forEach((group) => { nodeMap.set(group.id, { ...group, children: [] }); }); groups.forEach((group) => { const node = nodeMap.get(group.id)!; if (group.parentId && nodeMap.has(group.parentId)) { const parent = nodeMap.get(group.parentId)!; parent.children.push(node); } else { rootNodes.push(node); } }); const sortNodes = (nodes: UserGroupNode[]) => { nodes.sort((a, b) => a.name.localeCompare(b.name)); nodes.forEach((node) => sortNodes(node.children)); }; sortNodes(rootNodes); return rootNodes; } interface UserGroupTreeNodeProps { node: UserGroupNode; level: number; onSelect: (groupId: string) => void; selectedGroupId: string | null; onDelete: (groupId: string, groupName: string) => void; onAddSubGroup: (parentId: string) => void; } const UserGroupTreeNode: React.FC = ({ node, level, onSelect, selectedGroupId, onDelete, onAddSubGroup, }) => { const [isExpanded, setIsExpanded] = useState(true); const hasChildren = node.children && node.children.length > 0; return ( <> onSelect(node.id)} >
{hasChildren && ( )} {!hasChildren &&
} {node.name} {node.unitType || "Team"}
{node.members?.length || 0} {isExpanded && hasChildren && node.children.map((child) => ( ))} ); }; export function TenantUserGroupsTab() { const params = useParams<{ tenantId: string }>(); const tenantId = params.tenantId ?? ""; const [newGroupName, setNewGroupName] = useState(""); const [newGroupDesc, setNewGroupNameDesc] = useState(""); const [newGroupUnitType, setNewGroupUnitType] = useState("Team"); const [newGroupParentId, setNewGroupParentId] = useState(null); const [selectedGroupId, setSelectedGroupId] = useState(null); const groupsQuery = useQuery({ queryKey: ["groups", tenantId], queryFn: () => fetchGroups(tenantId), enabled: !!tenantId, }); const createMutation = useMutation({ mutationFn: () => createGroup(tenantId, { name: newGroupName, description: newGroupDesc, unitType: newGroupUnitType, parentId: newGroupParentId || undefined, }), onSuccess: () => { toast.success( t( "msg.admin.groups.list.create_success", "그룹이 성공적으로 생성되었습니다.", ), ); groupsQuery.refetch(); setNewGroupName(""); setNewGroupNameDesc(""); setNewGroupUnitType("Team"); setNewGroupParentId(null); }, onError: (error: AxiosError<{ error?: string }>) => { toast.error(t("msg.admin.groups.list.create_error", "그룹 생성 실패"), { description: error.response?.data?.error || error.message, }); }, }); const deleteMutation = useMutation({ mutationFn: (id: string) => deleteGroup(tenantId, id), onSuccess: () => { toast.success( t("msg.admin.groups.list.delete_success", "그룹이 삭제되었습니다."), ); groupsQuery.refetch(); if ( selectedGroupId && selectedGroupId === (deleteMutation.variables as any) ) { setSelectedGroupId(null); } }, onError: (error: AxiosError<{ error?: string }>) => { toast.error(t("msg.common.error", "그룹 삭제 실패"), { description: error.response?.data?.error || error.message, }); }, }); const addMemberMutation = useMutation({ mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => addGroupMember(tenantId, groupId, userId), onSuccess: () => { toast.success( t("msg.admin.groups.members.add_success", "멤버가 추가되었습니다."), ); groupsQuery.refetch(); }, onError: (error: AxiosError<{ error?: string }>) => { toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message, }); }, }); const removeMemberMutation = useMutation({ mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => removeGroupMember(tenantId, groupId, userId), onSuccess: () => { toast.success( t("msg.admin.groups.members.remove_success", "멤버가 제거되었습니다."), ); groupsQuery.refetch(); }, onError: (error: AxiosError<{ error?: string }>) => { toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message, }); }, }); const groupTree = groupsQuery.data ? buildGroupTree(groupsQuery.data) : []; const handleAddSubGroup = (parentId: string) => { setNewGroupParentId(parentId); }; const handleDeleteGroup = (groupId: string, groupName: string) => { if ( window.confirm( t( "msg.admin.groups.list.delete_confirm", `그룹 "{{name}}"을(를) 삭제하시겠습니까?`, { name: groupName }, ), ) ) { deleteMutation.mutate(groupId); } }; const handleAddMember = (groupId: string) => { const userId = window.prompt( t( "msg.admin.groups.prompt.user_id", "추가할 사용자의 UUID를 입력하세요:", ), ); if (userId) { addMemberMutation.mutate({ groupId, userId }); } }; const currentGroup = groupsQuery.data?.find((g) => g.id === selectedGroupId); return (
{" "} {t("ui.admin.groups.create.title", "새 그룹 생성")}
setNewGroupName(e.target.value)} />
setNewGroupUnitType(e.target.value)} />
setNewGroupNameDesc(e.target.value)} />
{t("ui.admin.groups.list.title", "User Groups")} {t( "msg.admin.groups.list.subtitle", "이 테넌트에 정의된 사용자 그룹 목록입니다.", )}
{t("ui.admin.groups.table.name", "NAME")} {t("ui.admin.groups.table.members", "MEMBERS")} {t("ui.admin.groups.table.actions", "ACTIONS")} {groupsQuery.isLoading && ( {t("msg.admin.groups.list.loading", "로딩 중...")} )} {!groupsQuery.isLoading && groupTree.length === 0 && ( {t( "msg.admin.groups.list.empty", "아직 등록된 그룹이 없습니다.", )} )} {groupTree.map((node) => ( ))}
{currentGroup && ( {" "} {t("msg.admin.groups.members.title", "[{{name}}] 멤버 관리", { name: currentGroup.name, })}
{t("ui.admin.groups.members.table.name", "이름")} {t("ui.admin.groups.members.table.email", "이메일")} {t("ui.admin.groups.members.table.remove", "제거")} {currentGroup.members?.length === 0 && ( {t("msg.admin.groups.members.empty", "멤버가 없습니다.")} )} {currentGroup.members?.map((user) => ( {user.name} {user.email} ))}
)}
); } export default TenantUserGroupsTab;