import { useEffect, useMemo, useRef, 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 { PasswordField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { createSystemAccount, fetchSystemAccountsDelta, fetchTenants, updateSystemAccount, updateSystemAccountRoles, updateSystemMemberships, type RoleSummary, type SystemAccountItem, type SystemMembershipDraft, type TenantAdminItem } from "../../api/admin"; import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels, i18nMessage, mergeDeltaRows, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; const emptyDraft = { email: "", displayName: "", password: "", passwordResetRequired: true, isActive: true, roleIds: [] as string[], memberships: [] as SystemMembershipDraft[] }; export default function SystemUsersPanel({ settings, canCreate, canUpdate, canSuspend, canAssignRoles, canManageMemberships, onAuthRefresh }: {settings: ApiSettings;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;canAssignRoles: boolean;canManageMemberships: boolean;onAuthRefresh: () => Promise;}) { const [accounts, setAccounts] = useState([]); const [roles, setRoles] = useState([]); const [tenants, setTenants] = useState([]); const accountsRef = useRef([]); const rolesRef = useRef([]); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const [editing, setEditing] = useState(null); const [viewing, setViewing] = useState(null); const [deactivating, setDeactivating] = useState(null); const [temporaryPassword, setTemporaryPassword] = useState<{email: string;value: string;} | null>(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; useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: closeEditor }); async function load() { setLoading(true); setError(""); try { let nextWatermark = getDeltaWatermark("access:system-accounts"); let nextAccounts = accountsRef.current; let nextRoles = rolesRef.current; let hasMore = false; do { const response = await fetchSystemAccountsDelta(settings, { since: nextWatermark }); const continuingFullSnapshot = response.full && nextWatermark?.startsWith("full:"); nextAccounts = response.full ? continuingFullSnapshot ? mergeDeltaRows(nextAccounts, response.accounts, [], (account) => account.account_id, { deletedResourceType: "access_system_account", sort: sortSystemAccounts }) : response.accounts : mergeDeltaRows(nextAccounts, response.accounts, response.deleted, (account) => account.account_id, { deletedResourceType: "access_system_account", sort: sortSystemAccounts }); nextRoles = response.full ? continuingFullSnapshot ? mergeDeltaRows(nextRoles, response.roles, [], (role) => role.id, { deletedResourceType: "access_system_role", sort: sortSystemRoles }) : response.roles : mergeDeltaRows(nextRoles, response.roles, response.deleted, (role) => role.id, { deletedResourceType: "access_system_role", sort: sortSystemRoles }); nextWatermark = response.watermark ?? null; hasMore = response.has_more; } while (hasMore); setDeltaWatermark("access:system-accounts", nextWatermark); const nextTenants = await fetchTenants(settings); accountsRef.current = nextAccounts; rolesRef.current = nextRoles; setAccounts(nextAccounts); setRoles(nextRoles); setTenants(nextTenants); } catch (err) {setError(adminErrorMessage(err));} finally {setLoading(false);} } useEffect(() => { accountsRef.current = []; rolesRef.current = []; resetDeltaWatermark(); void load(); }, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]); function openCreate() { setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); setEditing("new"); } function openEdit(item: SystemAccountItem) { const nextDraft = { email: item.email, displayName: item.display_name || "", password: "", passwordResetRequired: false, isActive: item.is_active, roleIds: item.roles.map((role) => role.id), memberships: item.memberships.map((membership) => ({ tenant_id: membership.tenant_id, is_active: membership.is_active, role_ids: membership.role_ids || [], group_ids: membership.group_ids || [], is_owner: membership.is_owner, is_last_active_owner: membership.is_last_active_owner })) }; setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); setEditing(item); } function closeEditor() { setEditing(null); setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); } function membership(tenantId: string) { return draft.memberships.find((item) => item.tenant_id === tenantId); } function setMembership(tenantId: string, enabled: boolean) { setDraft((current) => ({ ...current, memberships: enabled ? [...current.memberships.filter((item) => item.tenant_id !== tenantId), { tenant_id: tenantId, is_active: true, role_ids: [], group_ids: [] }] : current.memberships.filter((item) => item.tenant_id !== tenantId) })); } async function save(): Promise { setBusy(true); setError(""); try { if (editing === "new") { const response = await createSystemAccount(settings, { email: draft.email, display_name: draft.displayName || null, password: draft.password || null, password_reset_required: draft.passwordResetRequired, is_active: draft.isActive, role_ids: canAssignRoles ? draft.roleIds : [], memberships: canManageMemberships ? draft.memberships.map(({ tenant_id, is_active, role_ids, group_ids }) => ({ tenant_id, is_active, role_ids, group_ids })) : [] }); if (response.temporary_password) setTemporaryPassword({ email: response.account.email, value: response.temporary_password }); setSuccess(i18nMessage("i18n:govoplan-access.global_account_value_created.5100e467", { value0: response.account.email })); } else if (editing) { const accountChanges: {display_name?: string | null;is_active?: boolean;} = {}; if (canUpdate) accountChanges.display_name = draft.displayName || null; if (canSuspend) accountChanges.is_active = draft.isActive; if (Object.keys(accountChanges).length) await updateSystemAccount(settings, editing.account_id, accountChanges); if (canAssignRoles) await updateSystemAccountRoles(settings, editing.account_id, draft.roleIds); if (canManageMemberships) await updateSystemMemberships(settings, editing.account_id, draft.memberships.map(({ tenant_id, is_active, role_ids, group_ids }) => ({ tenant_id, is_active, role_ids, group_ids }))); setSuccess(i18nMessage("i18n:govoplan-access.global_account_value_updated.0de76b0e", { value0: editing.email })); } setEditing(null); await load(); await onAuthRefresh(); return true; } catch (err) {setError(adminErrorMessage(err));return false;} finally {setBusy(false);} } async function deactivate() { if (!deactivating) return; setBusy(true); setError(""); try { await updateSystemAccount(settings, deactivating.account_id, { is_active: false }); setSuccess(i18nMessage("i18n:govoplan-access.value_deactivated.ed3d027c", { value0: deactivating.email })); setDeactivating(null); await load(); await onAuthRefresh(); } catch (err) {setError(adminErrorMessage(err));} finally {setBusy(false);} } const columns = useMemo[]>(() => [ { id: "account", header: "i18n:govoplan-access.account.85dfa32c", width: "minmax(240px, 1.2fr)", minWidth: 210, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.display_name || ""} ${row.email}`, render: (row) =>
{row.display_name || row.email}
{row.email}
}, { id: "tenants", header: "i18n:govoplan-access.tenant_memberships.451de736", width: 280, minWidth: 190, maxWidth: 520, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => row.memberships.map((item) => item.tenant_name).join(", ") || "—" }, { id: "roles", header: "i18n:govoplan-access.system_roles.a9461aa6", width: 220, minWidth: 170, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => joinLabels(row.roles) }, { id: "status", header: "i18n:govoplan-access.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => }, { id: "last_login", header: "i18n:govoplan-access.last_login.43dab84f", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.last_login_at || "", render: (row) => formatDateTime(row.last_login_at) }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) =>
} onClick={() => setViewing(row)} /> } onClick={() => openEdit(row)} disabled={!(canUpdate || canSuspend || canAssignRoles || canManageMemberships)} /> } variant="danger" onClick={() => setDeactivating(row)} disabled={!canSuspend || !row.is_active || row.memberships.some((membership) => membership.is_last_active_owner)} />
}], [canAssignRoles, canManageMemberships, canSuspend, canUpdate]); return ( <> } variant="primary" onClick={openCreate} disabled={!canCreate} />}>
row.account_id} emptyText="i18n:govoplan-access.no_global_accounts_found.29d96a9e" />
!busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, email: event.target.value })} /> setDraft({ ...draft, displayName: event.target.value })} /> {editing === "new" && setDraft({ ...draft, password })} /> }
i18n:govoplan-access.system_roles.a9461aa6 ({ id: role.id, label: role.name, description: role.description, disabled: !canAssignRoles }))} selected={draft.roleIds} onChange={(roleIds) => setDraft({ ...draft, roleIds })} />
i18n:govoplan-access.tenant_memberships.451de736
{tenants.map((tenant) => )}
{editing && editing !== "new" && editing.memberships.some((membership) => membership.is_last_active_owner) &&

i18n:govoplan-access.this_account_is_the_last_active_operational_owne.5087839f

}

i18n:govoplan-access.removing_a_tenant_checkbox_suspends_that_members.7c6df77d

setViewing(null)} className="admin-dialog admin-dialog-wide" footer={}> {viewing &&
i18n:govoplan-access.account.85dfa32c
{viewing.email}
i18n:govoplan-access.display_name.c7874aaa
{viewing.display_name || "—"}
i18n:govoplan-access.status.bae7d5be
{viewing.is_active ? "i18n:govoplan-access.active.a733b809" : "i18n:govoplan-access.inactive.09af574c"}
i18n:govoplan-access.last_login.43dab84f
{formatDateTime(viewing.last_login_at)}
i18n:govoplan-access.system_roles.a9461aa6
{joinLabels(viewing.roles)}
i18n:govoplan-access.tenants.1f7ae776
{viewing.memberships.map((item) => item.tenant_name).join(", ") || "—"}
}
setTemporaryPassword(null)} className="admin-dialog" footer={}> {temporaryPassword && <>

i18n:govoplan-access.this_is_shown_once_for.b0f0f526 {temporaryPassword.email}.

{temporaryPassword.value}}
setDeactivating(null)} onConfirm={() => void deactivate()} /> ); } function draftKey(draft: typeof emptyDraft): string { return JSON.stringify(draft); } function sortSystemAccounts(left: SystemAccountItem, right: SystemAccountItem): number { return left.email.localeCompare(right.email); } function sortSystemRoles(left: RoleSummary, right: RoleSummary): number { return left.name.localeCompare(right.name); }