forked from baron/baron-sso
feat: enhance multi-tenant UI and fix member aggregation
- adminfront: Update TenantListPage and UserListPage sorting logic to support all columns dynamically - adminfront: Add inline 'Move', 'Remove', and 'Delete' actions directly inside the Tenant Org Chart member table - adminfront: Remove redundant 'ACTIONS' column and profile icon from UserListPage, making the name clickable - adminfront: Remove 'New Member' creation link from Tenant Org Chart 'Add Member' dropdown - backend: Fix CountByCompanyCodes query to accurately aggregate user counts using both primary company_code and company_codes array
This commit is contained in:
@@ -195,7 +195,9 @@ const SidebarNode: React.FC<{
|
||||
const MemberTable: React.FC<{
|
||||
tenantSlug: string;
|
||||
onRefreshTrigger?: number;
|
||||
}> = ({ tenantSlug, onRefreshTrigger }) => {
|
||||
allTenants?: TenantSummary[];
|
||||
}> = ({ tenantSlug, onRefreshTrigger, allTenants }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ["tenant-members-v2", tenantSlug, onRefreshTrigger],
|
||||
queryFn: () => fetchUsers(100, 0, undefined, tenantSlug),
|
||||
@@ -204,6 +206,54 @@ const MemberTable: React.FC<{
|
||||
|
||||
const members = data?.items ?? [];
|
||||
|
||||
const [isMoveOpen, setIsMoveOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<UserSummary | null>(null);
|
||||
const [targetTenantSlug, setTargetTenantSlug] = useState("");
|
||||
const [searchTenant, setSearchTenant] = useState("");
|
||||
|
||||
const moveMutation = useMutation({
|
||||
mutationFn: (newSlug: string) => {
|
||||
if (!selectedUser) throw new Error("No user selected");
|
||||
return updateUser(selectedUser.id, { tenantSlug: newSlug });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] });
|
||||
toast.success(
|
||||
t("msg.info.saved_success", "사용자 조직이 변경되었습니다."),
|
||||
);
|
||||
setIsMoveOpen(false);
|
||||
setSelectedUser(null);
|
||||
refetch();
|
||||
},
|
||||
onError: () => toast.error(t("msg.common.error", "오류가 발생했습니다.")),
|
||||
});
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: (userId: string) => updateUser(userId, { tenantSlug: "" }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] });
|
||||
toast.success(t("msg.info.saved_success", "조직에서 제외되었습니다."));
|
||||
refetch();
|
||||
},
|
||||
onError: () => toast.error(t("msg.common.error", "오류가 발생했습니다.")),
|
||||
});
|
||||
|
||||
const handleMoveClick = (user: UserSummary) => {
|
||||
setSelectedUser(user);
|
||||
setTargetTenantSlug("");
|
||||
setIsMoveOpen(true);
|
||||
};
|
||||
|
||||
const filteredTenants = React.useMemo(() => {
|
||||
if (!allTenants) return [];
|
||||
if (!searchTenant) return allTenants;
|
||||
return allTenants.filter(
|
||||
(t) =>
|
||||
t.name.toLowerCase().includes(searchTenant.toLowerCase()) ||
|
||||
t.slug.toLowerCase().includes(searchTenant.toLowerCase()),
|
||||
);
|
||||
}, [allTenants, searchTenant]);
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="py-20 text-center text-muted-foreground animate-pulse">
|
||||
@@ -264,6 +314,28 @@ const MemberTable: React.FC<{
|
||||
{t("ui.common.detail", "상세보기")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => handleMoveClick(user)}>
|
||||
<ArrowRight size={14} className="mr-2" />
|
||||
{t("ui.common.move_org", "타 조직으로 이동")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (
|
||||
window.confirm(
|
||||
t(
|
||||
"msg.admin.users.confirm_remove_org",
|
||||
"이 조직에서 사용자를 제외하시겠습니까?",
|
||||
),
|
||||
)
|
||||
) {
|
||||
removeMutation.mutate(user.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} className="mr-2" />
|
||||
{t("ui.common.remove_org", "조직에서 제외")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
@@ -271,6 +343,65 @@ const MemberTable: React.FC<{
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Dialog open={isMoveOpen} onOpenChange={setIsMoveOpen}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("ui.common.move_org", "타 조직으로 이동")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedUser?.name} 사용자를 이동할 타 조직을 선택하세요.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Input
|
||||
placeholder={t("ui.common.search", "조직 검색...")}
|
||||
value={searchTenant}
|
||||
onChange={(e) => setSearchTenant(e.target.value)}
|
||||
/>
|
||||
<ScrollArea className="h-48 border rounded-md p-2">
|
||||
<div className="space-y-1">
|
||||
{filteredTenants.map((tItem) => (
|
||||
<Button
|
||||
key={tItem.id}
|
||||
variant={
|
||||
targetTenantSlug === tItem.slug ? "secondary" : "ghost"
|
||||
}
|
||||
className="w-full justify-start text-sm"
|
||||
onClick={() => setTargetTenantSlug(tItem.slug)}
|
||||
>
|
||||
{React.createElement(getTenantIcon(tItem.type), {
|
||||
size: 14,
|
||||
className: "mr-2 opacity-70",
|
||||
})}
|
||||
<span>{tItem.name}</span>
|
||||
<span className="ml-2 text-[10px] text-muted-foreground opacity-50">
|
||||
{tItem.slug}
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
{filteredTenants.length === 0 && (
|
||||
<div className="py-4 text-center text-sm text-muted-foreground">
|
||||
{t("msg.common.no_results", "검색 결과가 없습니다.")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsMoveOpen(false)}>
|
||||
{t("ui.common.cancel", "취소")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => moveMutation.mutate(targetTenantSlug)}
|
||||
disabled={!targetTenantSlug || moveMutation.isPending}
|
||||
>
|
||||
{moveMutation.isPending ? "..." : t("ui.common.move", "이동")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -574,6 +705,7 @@ function TenantUserGroupsTab() {
|
||||
<MemberTable
|
||||
tenantSlug={selectedNode.slug}
|
||||
onRefreshTrigger={refreshMembersCount}
|
||||
allTenants={allTenantsData?.items ?? []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -702,6 +834,7 @@ const UserAddDialog: React.FC<{
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await updateUser(selectedUserId, { tenantSlug });
|
||||
queryClient.invalidateQueries({ queryKey: ["tenants-full-tree-v2"] });
|
||||
toast.success(t("msg.info.saved_success", "사용자가 배정되었습니다."));
|
||||
onOpenChange(false);
|
||||
resetFields();
|
||||
|
||||
Reference in New Issue
Block a user