1
0
forked from baron/baron-sso

조직도 기능 추가

This commit is contained in:
2026-04-10 11:38:47 +09:00
parent 6971b69b79
commit 5211842d47
28 changed files with 1845 additions and 447 deletions

View File

@@ -13,7 +13,7 @@ import {
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { OrgChartUploadModal } from "../components/OrgChartUploadModal";
import { Checkbox } from "../../../components/ui/checkbox";
import {
Table,
TableBody,
@@ -25,13 +25,17 @@ import {
import {
type TenantSummary,
deleteTenant,
deleteTenantsBulk,
fetchMe,
fetchTenants,
} from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";
import { OrgChartUploadModal } from "../components/OrgChartUploadModal";
function TenantListPage() {
const navigate = useNavigate();
const [selectedIds, setSelectedIds] = React.useState<string[]>([]);
const { data: profile } = useQuery({
queryKey: ["me"],
queryFn: fetchMe,
@@ -41,7 +45,6 @@ function TenantListPage() {
React.useEffect(() => {
if (profile?.role === "tenant_admin") {
const manageableCount = profile.manageableTenants?.length ?? 0;
// If only 1 in array, OR array is empty but we have a primary tenantId
if (
(manageableCount === 1 || manageableCount === 0) &&
profile.tenantId
@@ -67,6 +70,14 @@ function TenantListPage() {
},
});
const deleteBulkMutation = useMutation({
mutationFn: (ids: string[]) => deleteTenantsBulk(ids),
onSuccess: () => {
setSelectedIds([]);
query.refetch();
},
});
if (
profile &&
profile.role !== "super_admin" &&
@@ -84,7 +95,6 @@ function TenantListPage() {
);
}
// While redirecting (only if exactly one manageable tenant)
if (
profile?.role === "tenant_admin" &&
(profile.manageableTenants?.length ?? 0) <= 1
@@ -101,8 +111,40 @@ function TenantListPage() {
const tenants = query.data?.items ?? [];
// [New] Find a primary COMPANY_GROUP tenant to act as the root for matrix org charts
const rootTenant = tenants.find((t) => t.type === "COMPANY_GROUP") || tenants[0];
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedIds(tenants.map((t) => t.id));
} else {
setSelectedIds([]);
}
};
const handleSelect = (id: string, checked: boolean) => {
if (checked) {
setSelectedIds((prev) => [...prev, id]);
} else {
setSelectedIds((prev) => prev.filter((i) => i !== id));
}
};
const handleDeleteBulk = () => {
if (selectedIds.length === 0) return;
if (
!window.confirm(
t(
"msg.admin.tenants.delete_bulk_confirm",
"선택한 {{count}}개 테넌트를 삭제할까요?",
{ count: selectedIds.length },
),
)
) {
return;
}
deleteBulkMutation.mutate(selectedIds);
};
const rootTenant =
tenants.find((t) => t.type === "COMPANY_GROUP") || tenants[0];
const handleDelete = (tenantId: string, tenantName: string) => {
if (
@@ -134,16 +176,34 @@ function TenantListPage() {
</p>
</div>
<div className="flex items-center gap-2">
{/* [New] Add Upload Modal to global list page, visible to Super Admin */}
<RoleGuard roles={["super_admin"]}>
{rootTenant && (
<OrgChartUploadModal
tenantId={rootTenant.id}
onSuccess={() => query.refetch()}
/>
{selectedIds.length > 0 && (
<Button
variant="destructive"
onClick={handleDeleteBulk}
disabled={deleteBulkMutation.isPending}
className="gap-2"
>
<Trash2 size={16} />
{t("ui.admin.tenants.delete_selected", "선택 삭제")} (
{selectedIds.length})
</Button>
)}
</RoleGuard>
<RoleGuard roles={["super_admin"]}>
<OrgChartUploadModal
tenantId={rootTenant?.id || "root"}
onSuccess={() => query.refetch()}
/>
</RoleGuard>
<Button asChild variant="outline" className="gap-2">
<Link to="/tenants/org-chart">
{t("ui.admin.tenants.view_org_chart", "전체 조직도 보기")}
</Link>
</Button>
<Button
variant="outline"
onClick={() => query.refetch()}
@@ -191,6 +251,17 @@ function TenantListPage() {
<Table>
<TableHeader className="sticky top-0 z-10 bg-secondary shadow-sm">
<TableRow>
<TableHead className="w-[40px]">
<Checkbox
checked={
tenants.length > 0 &&
selectedIds.length === tenants.length
}
onCheckedChange={(checked) =>
handleSelectAll(!!checked)
}
/>
</TableHead>
<TableHead>
{t("ui.admin.tenants.table.name", "NAME")}
</TableHead>
@@ -217,7 +288,7 @@ function TenantListPage() {
<TableBody>
{query.isLoading && (
<TableRow>
<TableCell colSpan={7}>
<TableCell colSpan={8}>
{t("msg.common.loading", "로딩 중...")}
</TableCell>
</TableRow>
@@ -225,7 +296,7 @@ function TenantListPage() {
{!query.isLoading && tenants.length === 0 && (
<TableRow>
<TableCell
colSpan={7}
colSpan={8}
className="text-center py-8 text-muted-foreground"
>
{t(
@@ -237,6 +308,14 @@ function TenantListPage() {
)}
{tenants.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell>
<Checkbox
checked={selectedIds.includes(tenant.id)}
onCheckedChange={(checked) =>
handleSelect(tenant.id, !!checked)
}
/>
</TableCell>
<TableCell className="font-semibold">
{tenant.name}
</TableCell>