import { useEffect, useMemo, useState } from "react"; import { Search, Pencil, Plus, Trash2 } from "lucide-react"; import type { ApiSettings } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui"; import { ConfirmDialog } 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 { createGovernanceTemplate, deleteGovernanceTemplate, fetchGovernanceTemplates, fetchPermissionCatalog, fetchTenants, updateGovernanceTemplate, type GovernanceAssignment, type GovernanceTemplateItem, type PermissionItem, type TenantAdminItem } from "../../api/admin"; import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui"; const emptyDraft = { slug: "", name: "", description: "", isActive: true, permissions: [] as string[], assignments: [] as GovernanceAssignment[] }; export default function GovernanceTemplatesPanel({ settings, kind, canWrite, onAuthRefresh }: {settings: ApiSettings;kind: "group" | "role";canWrite: boolean;onAuthRefresh: () => Promise;}) { const [items, setItems] = useState([]); const [tenants, setTenants] = useState([]); const [permissions, setPermissions] = useState([]); const [editing, setEditing] = useState(null); const [viewing, setViewing] = useState(null); const [deleting, setDeleting] = useState(null); const [draft, setDraft] = useState(emptyDraft); const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft)); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const dirty = editing !== null && draftKey(draft) !== savedDraftKey; const permissionsByScope = useMemo(() => new Map(permissions.map((permission) => [permission.scope, permission])), [permissions]); useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: closeEditor }); async function load() { setLoading(true); setError(""); try { const [nextItems, nextTenants, nextPermissions] = await Promise.all([ fetchGovernanceTemplates(settings, kind), fetchTenants(settings), kind === "role" ? fetchPermissionCatalog(settings) : Promise.resolve([])] ); setItems(nextItems); setTenants(nextTenants); setPermissions(nextPermissions.filter((item) => item.level === "tenant")); } catch (err) {setError(adminErrorMessage(err));} finally {setLoading(false);} } useEffect(() => {void load();}, [settings.accessToken, settings.apiBaseUrl, kind]); function openCreate() { setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); setEditing("new"); } function openEdit(item: GovernanceTemplateItem) { const nextDraft = { slug: item.slug, name: item.name, description: item.description || "", isActive: item.is_active, permissions: item.permissions, assignments: item.assignments }; setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); setEditing(item); } function closeEditor() { setEditing(null); setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); } function assignmentMode(tenantId: string): "none" | "available" | "required" { return draft.assignments.find((item) => item.tenant_id === tenantId)?.mode ?? "none"; } function setAssignment(tenantId: string, mode: "none" | "available" | "required") { setDraft((current) => ({ ...current, assignments: mode === "none" ? current.assignments.filter((item) => item.tenant_id !== tenantId) : [...current.assignments.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, mode }] })); } async function save(): Promise { setBusy(true); setError(""); try { if (editing === "new") { await createGovernanceTemplate(settings, { kind, slug: draft.slug, name: draft.name, description: draft.description || null, permissions: kind === "role" ? draft.permissions : [], is_active: draft.isActive, assignments: draft.assignments }); setSuccess(i18nMessage("i18n:govoplan-admin.value_template_created.d68a5166", { value0: kind === "group" ? "i18n:govoplan-admin.group.171a0606" : "i18n:govoplan-admin.role.c3f104d1" })); } else if (editing) { await updateGovernanceTemplate(settings, editing.id, { name: draft.name, description: draft.description || null, permissions: kind === "role" ? draft.permissions : [], is_active: draft.isActive, assignments: draft.assignments }); setSuccess(i18nMessage("i18n:govoplan-admin.value_updated_and_synchronized_to_assigned_tenan.d136eef2", { value0: draft.name })); } setEditing(null); await load(); await onAuthRefresh(); return true; } catch (err) {setError(adminErrorMessage(err));return false;} finally {setBusy(false);} } async function remove() { if (!deleting) return; setBusy(true); setError(""); try { await deleteGovernanceTemplate(settings, deleting.id); setSuccess(i18nMessage("i18n:govoplan-admin.value_deleted.3c4bf574", { value0: deleting.name })); setDeleting(null); await load(); await onAuthRefresh(); } catch (err) {setError(adminErrorMessage(err));} finally {setBusy(false);} } const columns = useMemo[]>(() => [ { id: "template", header: kind === "group" ? "i18n:govoplan-admin.group_template.973e0fa6" : "i18n:govoplan-admin.tenant_role.6b53115d", width: "minmax(240px, 1.2fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.name} ${row.slug}`, render: (row) =>
{row.name}
{row.slug}
}, { id: "tenants", header: "i18n:govoplan-admin.tenant_availability.067a4f31", width: 320, minWidth: 210, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => row.assignments.map((assignment) => tenants.find((tenant) => tenant.id === assignment.tenant_id)?.name || assignment.tenant_id).join(", ") || "—", render: (row) => row.assignments.length ? row.assignments.map((assignment) => { const tenant = tenants.find((item) => item.id === assignment.tenant_id); return i18nMessage("i18n:govoplan-admin.value_value.c189e8bc", { value0: tenant?.name || assignment.tenant_id, value1: assignment.mode }); }).join(", ") : "—" }, ...(kind === "role" ? [{ id: "permissions", header: "i18n:govoplan-admin.permissions.d06d5557", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer" as const, value: (row: GovernanceTemplateItem) => row.effective_permission_count }] : []), { id: "status", header: "i18n:govoplan-admin.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => }, { id: "actions", header: "i18n:govoplan-admin.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, { id: "edit", label: i18nMessage("i18n:govoplan-admin.edit_value.fad75899", { value0: row.name }), icon: , disabled: !canWrite, onClick: () => openEdit(row) }, { id: "delete", label: i18nMessage("i18n:govoplan-admin.delete_value.4d18989e", { value0: row.name }), icon: , variant: "danger", disabled: !canWrite, onClick: () => setDeleting(row) } ]} /> }], [canWrite, kind, tenants]); const title = kind === "group" ? "i18n:govoplan-admin.central_groups.5c9b5b66" : "i18n:govoplan-admin.tenant_roles.51aca82d"; const description = kind === "group" ? "i18n:govoplan-admin.centrally_defined_group_identities_that_are_prov.7761fce9" : "i18n:govoplan-admin.centrally_governed_tenant_roles_that_are_provisi.8739fca3"; return ( <> } variant="primary" onClick={openCreate} disabled={!canWrite} />}>
row.id} emptyText={i18nMessage("i18n:govoplan-admin.no_central_value_templates_found.081149fa", { value0: kind })} />
!busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, slug: event.target.value })} />