import { useEffect, useMemo, useState } from "react"; import { Pencil, Plus, Search, Trash2 } from "lucide-react"; import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; import { createTenant, fetchSystemSettings, fetchTenantOwnerCandidates, fetchTenants, updateTenant, type SystemSettingsItem, type TenantAdminItem, type TenantOwnerCandidate } from "../../api/admin"; import { Button } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import { AdminIconButton, AdminPageLayout, adminErrorMessage, formatAdminDateTime as formatDateTime } from "@govoplan/core-webui"; type OverrideValue = "inherit" | "allow" | "deny"; type TenantDraft = { slug: string; name: string; ownerAccountId: string; description: string; defaultLocale: string; isActive: boolean; customGroups: OverrideValue; customRoles: OverrideValue; apiKeys: OverrideValue; }; const emptyDraft: TenantDraft = { slug: "", name: "", ownerAccountId: "", description: "", defaultLocale: "en", isActive: true, customGroups: "inherit", customRoles: "inherit", apiKeys: "inherit" }; function fromOverride(value?: boolean | null): OverrideValue { if (value === true) return "allow"; if (value === false) return "deny"; return "inherit"; } function toOverride(value: OverrideValue): boolean | null { if (value === "allow") return true; if (value === "deny") return false; return null; } export default function TenantsPanel({ settings, auth, canCreate, canUpdate, canSuspend, onAuthRefresh }: { settings: ApiSettings; auth: AuthInfo; canCreate: boolean; canUpdate: boolean; canSuspend: boolean; onAuthRefresh: () => Promise; }) { const [tenants, setTenants] = useState([]); const [systemSettings, setSystemSettings] = useState(null); const [ownerCandidates, setOwnerCandidates] = useState([]); const [editing, setEditing] = useState(null); const [viewing, setViewing] = useState(null); const [draft, setDraft] = useState(emptyDraft); const [confirmSuspend, setConfirmSuspend] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); async function load() { setLoading(true); setError(""); try { const [nextTenants, nextOwnerCandidates, nextSystemSettings] = await Promise.all([ fetchTenants(settings), canCreate ? fetchTenantOwnerCandidates(settings) : Promise.resolve([]), fetchSystemSettings(settings).catch(() => null) ]); setTenants(nextTenants); setOwnerCandidates(nextOwnerCandidates); setSystemSettings(nextSystemSettings); } catch (err) { setError(adminErrorMessage(err)); } finally { setLoading(false); } } useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]); function openCreate() { setDraft({ ...emptyDraft, ownerAccountId: auth.user.account_id }); setEditing("new"); setError(""); } function openEdit(tenant: TenantAdminItem) { setDraft({ slug: tenant.slug, name: tenant.name, ownerAccountId: "", description: tenant.description || "", defaultLocale: tenant.default_locale || "en", isActive: tenant.is_active, customGroups: fromOverride(tenant.allow_custom_groups), customRoles: fromOverride(tenant.allow_custom_roles), apiKeys: fromOverride(tenant.allow_api_keys) }); setEditing(tenant); setError(""); } async function save() { setBusy(true); setError(""); try { const governance = { allow_custom_groups: toOverride(draft.customGroups), allow_custom_roles: toOverride(draft.customRoles), allow_api_keys: toOverride(draft.apiKeys) }; if (editing === "new") { const created = await createTenant(settings, { slug: draft.slug, name: draft.name, owner_account_id: draft.ownerAccountId || null, description: draft.description || null, default_locale: draft.defaultLocale, settings: {}, ...governance }); const selectedOwner = ownerCandidates.find((candidate) => candidate.account_id === draft.ownerAccountId); setSuccess(`Tenant ${created.name} created with ${selectedOwner?.display_name || selectedOwner?.email || "the selected account"} as Owner.`); await onAuthRefresh(); } else if (editing) { const payload: Parameters[2] = {}; if (canUpdate) { payload.name = draft.name; payload.description = draft.description || null; payload.default_locale = draft.defaultLocale; payload.allow_custom_groups = governance.allow_custom_groups; payload.allow_custom_roles = governance.allow_custom_roles; payload.allow_api_keys = governance.allow_api_keys; } if (canSuspend) payload.is_active = draft.isActive; await updateTenant(settings, editing.id, payload); setSuccess(`Tenant ${draft.name} updated.`); await onAuthRefresh(); } setEditing(null); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(false); } } async function suspend() { if (!confirmSuspend) return; setBusy(true); setError(""); try { await updateTenant(settings, confirmSuspend.id, { is_active: false }); setSuccess(`${confirmSuspend.name} suspended.`); setConfirmSuspend(null); await onAuthRefresh(); await load(); } catch (err) { setError(adminErrorMessage(err)); } finally { setBusy(false); } } const activeTenantId = (auth.active_tenant ?? auth.tenant).id; const systemAllowsCustomGroups = systemSettings?.allow_tenant_custom_groups !== false; const systemAllowsCustomRoles = systemSettings?.allow_tenant_custom_roles !== false; const systemAllowsApiKeys = systemSettings?.allow_tenant_api_keys !== false; const systemDeniedGovernance = [ systemAllowsCustomGroups ? "" : "custom groups", systemAllowsCustomRoles ? "" : "custom roles", systemAllowsApiKeys ? "" : "API keys" ].filter(Boolean).join(", "); const columns = useMemo[]>(() => [ { id: "name", header: "Tenant", width: "minmax(210px, 1fr)", minWidth: 190, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.name} ${row.slug}`, render: (row) =>
{row.name}
{row.slug}
}, { id: "users", header: "Users", width: 100, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.users ?? 0, render: (row) => `${row.counts.active_users ?? 0}/${row.counts.users ?? 0}` }, { id: "groups", header: "Groups", width: 95, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.groups ?? 0 }, { id: "campaigns", header: "Campaigns", width: 110, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.campaigns ?? 0 }, { id: "files", header: "Files", width: 90, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.files ?? 0 }, { id: "locale", header: "Locale", width: 120, minWidth: 90, maxWidth: 220, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => row.default_locale }, { id: "status", header: "Status", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => }, { id: "actions", header: "Actions", width: 150, sticky: "end", resizable: false, align: "right", render: (row) =>
} onClick={() => setViewing(row)} /> } onClick={() => openEdit(row)} disabled={!canUpdate} /> } variant="danger" onClick={() => setConfirmSuspend(row)} disabled={!canSuspend || !row.is_active || row.id === activeTenantId} />
} ], [activeTenantId, canSuspend, canUpdate]); return ( <> } variant="primary" onClick={openCreate} disabled={!canCreate} />} >
row.id} emptyText="No tenants found." />
!busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, slug: event.target.value })} /> {editing === "new" && } setDraft({ ...draft, defaultLocale: event.target.value })} /> {editing !== "new" && }