forked from baron/baron-sso
217 lines
7.5 KiB
TypeScript
217 lines
7.5 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import type { AxiosError } from "axios";
|
|
import {
|
|
Loader2,
|
|
Mail,
|
|
MoreHorizontal,
|
|
Plus,
|
|
User,
|
|
UserMinus,
|
|
UserPlus,
|
|
} from "lucide-react";
|
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
|
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
|
|
import { Badge } from "../../../components/ui/badge";
|
|
import { Button } from "../../../components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "../../../components/ui/card";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "../../../components/ui/dropdown-menu";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "../../../components/ui/table";
|
|
import { toast } from "../../../components/ui/use-toast";
|
|
import { fetchTenant, fetchUsers, updateUser } from "../../../lib/adminApi";
|
|
import { t } from "../../../lib/i18n";
|
|
|
|
function TenantUsersPage() {
|
|
const params = useParams<{ tenantId: string }>();
|
|
const navigate = useNavigate();
|
|
const tenantId = params.tenantId ?? "";
|
|
const queryClient = useQueryClient();
|
|
|
|
// 테넌트의 슬러그(tenantSlug)를 먼저 가져옴
|
|
const tenantQuery = useQuery({
|
|
queryKey: ["tenant", tenantId],
|
|
queryFn: () => fetchTenant(tenantId),
|
|
enabled: tenantId.length > 0,
|
|
});
|
|
|
|
const tenantSlug = tenantQuery.data?.slug;
|
|
|
|
// 해당 슬러그로 사용자 검색
|
|
const usersQuery = useQuery({
|
|
queryKey: ["users", { tenantSlug }],
|
|
queryFn: () => fetchUsers(100, 0, undefined, tenantSlug),
|
|
enabled: !!tenantSlug,
|
|
});
|
|
|
|
const removeTenantMutation = useMutation({
|
|
mutationFn: ({ userId, slug }: { userId: string; slug: string }) =>
|
|
updateUser(userId, { tenantSlug: slug, isRemoveTenant: true }),
|
|
onSuccess: () => {
|
|
toast.success(
|
|
t(
|
|
"msg.admin.tenants.members.remove_success",
|
|
"조직에서 제외되었습니다.",
|
|
),
|
|
);
|
|
usersQuery.refetch();
|
|
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
|
},
|
|
onError: (err: AxiosError<{ error?: string }>) => {
|
|
toast.error(
|
|
err.response?.data?.error ||
|
|
t("msg.admin.tenants.members.remove_error", "제외 실패"),
|
|
);
|
|
},
|
|
});
|
|
|
|
const handleRemoveMember = (userId: string, userName: string) => {
|
|
if (!tenantSlug) return;
|
|
if (
|
|
window.confirm(
|
|
t(
|
|
"msg.admin.tenants.members.remove_confirm",
|
|
"'{{name}}'님을 이 조직에서 제외하시겠습니까?",
|
|
{ name: userName },
|
|
),
|
|
)
|
|
) {
|
|
removeTenantMutation.mutate({ userId, slug: tenantSlug });
|
|
}
|
|
};
|
|
|
|
const users = usersQuery.data?.items ?? [];
|
|
|
|
return (
|
|
<Card className="mt-6 bg-[var(--color-panel)] flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
<CardHeader className="flex-shrink-0 flex flex-row items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<User size={18} className="text-primary" />
|
|
{t("ui.admin.tenants.members.title", "Tenant Members ({{count}})", {
|
|
count: users.length,
|
|
})}
|
|
</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" size="sm" asChild className="gap-2">
|
|
<Link to={`/users?addTenant=${tenantSlug}`}>
|
|
<UserPlus size={16} />
|
|
{t("ui.admin.tenants.members.add_existing", "기존 멤버 배정")}
|
|
</Link>
|
|
</Button>
|
|
<Button size="sm" asChild className="gap-2">
|
|
<Link to={`/users/new?tenantSlug=${tenantSlug}`}>
|
|
<Plus size={16} />
|
|
{t("ui.admin.tenants.members.create_new", "신규 멤버 생성")}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="flex-1 flex flex-col min-h-0 pt-0">
|
|
<div className="flex-1 rounded-md border overflow-hidden flex flex-col">
|
|
<div className="flex-1 overflow-auto relative custom-scrollbar">
|
|
<Table>
|
|
<TableHeader className={commonStickyTableHeaderClass}>
|
|
<TableRow>
|
|
<TableHead>
|
|
{t("ui.admin.tenants.members.table.name", "NAME")}
|
|
</TableHead>
|
|
<TableHead>
|
|
{t("ui.admin.tenants.members.table.email", "EMAIL")}
|
|
</TableHead>
|
|
<TableHead>
|
|
{t("ui.admin.tenants.members.table.role", "ROLE")}
|
|
</TableHead>
|
|
<TableHead>
|
|
{t("ui.admin.tenants.members.table.status", "STATUS")}
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{usersQuery.isLoading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={4} className="text-center py-20">
|
|
<div className="flex flex-col items-center gap-2">
|
|
<Loader2
|
|
className="animate-spin text-muted-foreground"
|
|
size={24}
|
|
/>
|
|
<span className="text-sm text-muted-foreground">
|
|
{t("ui.common.loading", "Loading...")}
|
|
</span>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : users.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={4}
|
|
className="text-center py-8 text-muted-foreground"
|
|
>
|
|
{t(
|
|
"msg.admin.tenants.members.empty",
|
|
"소속된 사용자가 없습니다.",
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
users.map((user) => (
|
|
<TableRow
|
|
key={user.id}
|
|
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
|
onClick={() => navigate(`/users/${user.id}`)}
|
|
>
|
|
<TableCell className="font-semibold">
|
|
{user.name}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-1 text-xs">
|
|
<Mail size={12} className="text-muted-foreground" />
|
|
{user.email}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className="capitalize">
|
|
{t(
|
|
`ui.common.role.${user.role}`,
|
|
user.role.replace("_", " "),
|
|
)}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant={
|
|
user.status === "active" ? "default" : "muted"
|
|
}
|
|
>
|
|
{t(`ui.common.status.${user.status}`, user.status)}
|
|
</Badge>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default TenantUsersPage;
|