import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { AlertTriangle, FolderTree, Loader2, Search } from "lucide-react"; import * as React from "react"; import { Button } from "../../../components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "../../../components/ui/dialog"; import { Input } from "../../../components/ui/input"; import { ScrollArea } from "../../../components/ui/scroll-area"; import { toast } from "../../../components/ui/use-toast"; import { type GroupSummary, type TenantSummary, type UserSummary, bulkUpdateUsers, fetchAllTenants, fetchGroups, } from "../../../lib/adminApi"; import { t } from "../../../lib/i18n"; type UserSchemaField = { key: string; label: string; required?: boolean; }; interface UserBulkMoveGroupModalProps { userIds: string[]; selectedUsers?: UserSummary[]; onSuccess?: () => void; } export function UserBulkMoveGroupModal({ userIds, selectedUsers = [], onSuccess, }: UserBulkMoveGroupModalProps) { const [open, setOpen] = React.useState(false); const [selectedTenantSlug, setSelectedTenantSlug] = React.useState(""); const [selectedGroupName, setSelectedGroupName] = React.useState(""); const [searchTerm, setSearchTerm] = React.useState(""); const [acknowledgeWarning, setAcknowledgeWarning] = React.useState(false); const queryClient = useQueryClient(); const { data: tenantsData } = useQuery({ queryKey: ["tenants", "all"], queryFn: () => fetchAllTenants(), enabled: open, }); const tenants = tenantsData?.items ?? []; const selectedTenant = React.useMemo( () => tenants.find((t) => t.slug === selectedTenantSlug), [tenants, selectedTenantSlug], ); const selectedTenantId = selectedTenant?.id ?? ""; const { data: groups, isLoading: isGroupsLoading } = useQuery({ queryKey: ["tenant-groups", selectedTenantId], queryFn: () => fetchGroups(selectedTenantId), enabled: open && !!selectedTenantId, }); const schemaWarnings = React.useMemo(() => { if (!selectedTenant || selectedUsers.length === 0) return null; const targetSchema = (selectedTenant.config?.userSchema as UserSchemaField[]) || []; const targetSchemaKeys = new Set(targetSchema.map((f) => f.key)); const requiredKeys = targetSchema .filter((f) => f.required) .map((f) => f.key); const missingRequiredFields = new Set(); const incompatibleFields = new Set(); for (const user of selectedUsers) { const userMeta = user.metadata || {}; // 1. Check for missing required fields for (const key of requiredKeys) { if ( userMeta[key] === undefined || userMeta[key] === null || userMeta[key] === "" ) { missingRequiredFields.add(key); } } // 2. Check for fields that exist in user metadata but not in the target schema (data loss) for (const key of Object.keys(userMeta)) { if (!targetSchemaKeys.has(key)) { incompatibleFields.add(key); } } } if (missingRequiredFields.size === 0 && incompatibleFields.size === 0) { return null; } return { missing: Array.from(missingRequiredFields), incompatible: Array.from(incompatibleFields), }; }, [selectedTenant, selectedUsers]); const mutation = useMutation({ mutationFn: bulkUpdateUsers, onSuccess: () => { toast.success( t( "msg.admin.users.bulk.move_success", "사용자들의 부서가 이동되었습니다.", ), ); setOpen(false); onSuccess?.(); }, onError: (error: AxiosError<{ error?: string }>) => { toast.error(t("msg.admin.users.bulk.move_error", "부서 이동 실패"), { description: error.response?.data?.error || error.message, }); }, }); const handleMove = () => { if (!selectedTenantSlug) return; mutation.mutate({ userIds, tenantSlug: selectedTenantSlug, department: selectedGroupName, // can be empty for "No Department" }); }; const filteredGroups = React.useMemo(() => { if (!groups) return []; if (!searchTerm) return groups; return groups.filter((g) => g.name.toLowerCase().includes(searchTerm.toLowerCase()), ); }, [groups, searchTerm]); return ( { setOpen(val); if (!val) { setSelectedTenantSlug(""); setSelectedGroupName(""); setAcknowledgeWarning(false); setSearchTerm(""); } }} > {t("ui.admin.users.bulk.move_title", "사용자 부서 이동")} {t( "msg.admin.users.bulk.move_description", "선택한 {{count}}명의 사용자를 이동할 테넌트와 부서를 선택하세요.", { count: userIds.length }, )}
{selectedTenantSlug && (
setSearchTerm(e.target.value)} />
{isGroupsLoading ? (
) : ( filteredGroups.map((group) => ( )) )}
)} {schemaWarnings && (
{t("ui.admin.users.bulk.schema_warning", "스키마 호환성 경고")}
{schemaWarnings.missing.length > 0 && (

{t( "msg.admin.users.bulk.schema_missing", "대상 테넌트의 필수 필드가 누락되어 있습니다:", )}{" "} {schemaWarnings.missing.join(", ")}

)} {schemaWarnings.incompatible.length > 0 && (

{t( "msg.admin.users.bulk.schema_incompatible", "대상 테넌트 스키마에 없는 필드는 유실될 수 있습니다:", )}{" "} {schemaWarnings.incompatible.join(", ")}

)}
)}
); }