import { useEffect, useMemo, useRef, useState } from "react"; import { Pencil, Plus, Search, Trash2 } from "lucide-react"; import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; import { createGroup, fetchGroupsDelta, fetchRolesDelta, fetchUsersDelta, updateGroup, type GroupSummary, type RoleSummary, type UserAdminItem } 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, AdminSelectionList, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; const emptyDraft = { slug: "", name: "", description: "", isActive: true, memberIds: [] as string[], roleIds: [] as string[] }; export default function GroupsPanel({ settings, auth, canDefine, canManageMembers, canAssignRoles, onAuthRefresh }: {settings: ApiSettings;auth: AuthInfo;canDefine: boolean;canManageMembers: boolean;canAssignRoles: boolean;onAuthRefresh: () => Promise;}) { const [groups, setGroups] = useState([]); const [users, setUsers] = useState([]); const [roles, setRoles] = useState([]); const groupsRef = useRef([]); const usersRef = 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 [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 { const [nextGroups, nextUsers, nextRoles] = await Promise.all([ loadDeltaRows(groupsRef.current, "access:groups", getDeltaWatermark, setDeltaWatermark, (since) => fetchGroupsDelta(settings, { since }), (response) => response.groups, (group) => group.id, "access_group", sortGroups), loadDeltaRows(usersRef.current, "access:users", getDeltaWatermark, setDeltaWatermark, (since) => fetchUsersDelta(settings, { since }), (response) => response.users, (user) => user.id, "access_user", sortUsers), loadDeltaRows(rolesRef.current, "access:roles", getDeltaWatermark, setDeltaWatermark, (since) => fetchRolesDelta(settings, { since }), (response) => response.roles, (role) => role.id, "access_role", sortTenantRoles)] ); groupsRef.current = nextGroups; usersRef.current = nextUsers; rolesRef.current = nextRoles; setGroups(nextGroups); setUsers(nextUsers); setRoles(nextRoles.filter((role) => role.is_assignable)); } catch (err) {setError(adminErrorMessage(err));} finally {setLoading(false);} } useEffect(() => { groupsRef.current = []; usersRef.current = []; rolesRef.current = []; resetDeltaWatermark(); void load(); }, [settings.accessToken, settings.apiBaseUrl, (auth.active_tenant ?? auth.tenant).id, resetDeltaWatermark]); function openCreate() {setDraft(emptyDraft);setSavedDraftKey(draftKey(emptyDraft));setEditing("new");setError("");} function openEdit(group: GroupSummary) { const nextDraft = { slug: group.slug, name: group.name, description: group.description || "", isActive: group.is_active, memberIds: group.member_ids, roleIds: group.roles.map((role) => role.id) }; setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); setEditing(group); setError(""); } function closeEditor() { setEditing(null); setDraft(emptyDraft); setSavedDraftKey(draftKey(emptyDraft)); } async function save(): Promise { setBusy(true); setError(""); try { if (editing === "new") { await createGroup(settings, { slug: draft.slug, name: draft.name, description: draft.description || null, is_active: draft.isActive, member_ids: canManageMembers ? draft.memberIds : [], role_ids: canAssignRoles ? draft.roleIds : [] }); setSuccess(i18nMessage("i18n:govoplan-access.group_value_created.5a39a341", { value0: draft.name })); } else if (editing) { const managed = Boolean(editing.system_template_id); await updateGroup(settings, editing.id, { ...(canDefine && !managed ? { name: draft.name, description: draft.description || null } : {}), ...(canDefine && !editing.system_required ? { is_active: draft.isActive } : {}), ...(canManageMembers ? { member_ids: draft.memberIds } : {}), ...(canAssignRoles ? { role_ids: draft.roleIds } : {}) }); setSuccess(i18nMessage("i18n:govoplan-access.group_value_updated.3d97d5b2", { value0: draft.name })); } setEditing(null); await onAuthRefresh(); await load(); return true; } catch (err) {setError(adminErrorMessage(err));return false;} finally {setBusy(false);} } async function deactivate() { if (!deactivating) return; setBusy(true); setError(""); try { await updateGroup(settings, deactivating.id, { is_active: false }); setSuccess(i18nMessage("i18n:govoplan-access.group_value_deactivated.8512bf9c", { value0: deactivating.name })); setDeactivating(null); if (deactivating.member_ids.includes(auth.user.id)) await onAuthRefresh(); await load(); } catch (err) {setError(adminErrorMessage(err));} finally {setBusy(false);} } const columns = useMemo[]>(() => [ { id: "group", header: "i18n:govoplan-access.group.171a0606", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.name} ${row.slug}`, render: (row) =>
{row.name}
{row.slug} {row.system_template_id && }
}, { id: "members", header: "i18n:govoplan-access.members.1cb449c1", width: 110, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.member_count }, { id: "roles", header: "i18n:govoplan-access.inherited_roles.8def9f05", width: 260, minWidth: 180, maxWidth: 520, resizable: true, fill: 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: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , disabled: !(canDefine || canManageMembers || canAssignRoles), onClick: () => openEdit(row) }, { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.name }), icon: , variant: "danger", applicable: row.is_active, disabled: !canDefine || Boolean(row.system_required), onClick: () => setDeactivating(row) } ]} /> }], [canAssignRoles, canDefine, canManageMembers]); return ( <> } variant="primary" onClick={openCreate} disabled={!canDefine} />}>
row.id} emptyText="i18n:govoplan-access.no_groups_found.627ca913" />
!busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}> {editing !== "new" && editing?.system_template_id &&

i18n:govoplan-access.this_group_definition_is_managed_by_the_system_n.640b235e

}
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, slug: event.target.value })} />