import { useEffect, useMemo, useRef, useState } from "react"; import { Archive, Check, ChevronRight, Pencil, Plus, RefreshCw, Save, Send, Trash2 } from "lucide-react"; import { AdminPageLayout, Button, ConfirmDialog, Dialog, DismissibleAlert, FormField, IconButton, StatusBadge, ToggleSwitch, adminErrorMessage, dispatchPlatformViewChanged, useUnsavedChanges, useUnsavedDraftGuard, useViewSurfaces, type ApiSettings, type PlatformViewSurface } from "@govoplan/core-webui"; import { archiveViewDefinition, createViewAssignment, createViewDefinition, createViewRevision, deleteViewAssignment, fetchViewAssignments, fetchViewDefinitions, publishViewRevision, updateViewAssignment, updateViewDefinition, type ViewAssignment, type ViewAssignmentMode, type ViewAssignmentScopeType, type ViewDefinition, type ViewScopeType } from "../../api/views"; type DefinitionDraft = { name: string; description: string; surfaceIds: string[]; }; type AssignmentDraft = { scopeType: ViewAssignmentScopeType; scopeId: string; definitionId: string; mode: ViewAssignmentMode; priority: number; active: boolean; pinRevision: boolean; }; const LOCKOUT_SURFACES = new Set([ "access.module", "access.nav.admin", "access.route.admin", "views.module", "views.selector", "views.admin.system", "views.admin.tenant" ]); export default function ViewsAdminPanel({ settings, scopeType, canWriteDefinitions, canWriteAssignments }: { settings: ApiSettings; scopeType: ViewScopeType; canWriteDefinitions: boolean; canWriteAssignments: boolean; }) { const surfaces = useViewSurfaces(); const { requestDiscard } = useUnsavedChanges(); const [definitions, setDefinitions] = useState([]); const [assignments, setAssignments] = useState([]); const [selectedId, setSelectedId] = useState(""); const [draft, setDraft] = useState({ name: "", description: "", surfaceIds: [] }); const [savedDraftKey, setSavedDraftKey] = useState(""); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const [createOpen, setCreateOpen] = useState(false); const [createDraft, setCreateDraft] = useState({ name: "", description: "" }); const [assignmentEditor, setAssignmentEditor] = useState< ViewAssignment | "new" | null >(null); const [assignmentDraft, setAssignmentDraft] = useState( emptyAssignmentDraft(scopeType) ); const [archiveTarget, setArchiveTarget] = useState(null); const [deleteAssignmentTarget, setDeleteAssignmentTarget] = useState(null); const selected = definitions.find((item) => item.id === selectedId) ?? null; const dirty = Boolean( selected && !selected.readonly && definitionDraftKey(draft) !== savedDraftKey ); useUnsavedDraftGuard({ dirty, onSave: saveDraft, onDiscard: resetDraft }); async function load(preferredId?: string) { setLoading(true); setError(""); try { const [nextDefinitions, nextAssignments] = await Promise.all([ fetchViewDefinitions(settings, scopeType), fetchViewAssignments(settings, scopeType) ]); setDefinitions(nextDefinitions); setAssignments(nextAssignments); const nextSelectedId = preferredId && nextDefinitions.some((item) => item.id === preferredId) ? preferredId : nextDefinitions.some((item) => item.id === selectedId) ? selectedId : nextDefinitions[0]?.id ?? ""; setSelectedId(nextSelectedId); const nextSelected = nextDefinitions.find((item) => item.id === nextSelectedId) ?? null; applyDefinitionDraft(nextSelected); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setLoading(false); } } useEffect(() => { setSelectedId(""); setAssignmentDraft(emptyAssignmentDraft(scopeType)); void load(); }, [ scopeType, settings.accessToken, settings.apiBaseUrl, settings.apiKey ]); function applyDefinitionDraft(definition: ViewDefinition | null) { const next = definition ? { name: definition.name, description: definition.description ?? "", surfaceIds: definition.latest_revision.visible_surface_ids } : { name: "", description: "", surfaceIds: [] }; setDraft(next); setSavedDraftKey(definitionDraftKey(next)); } function resetDraft() { applyDefinitionDraft(selected); } function selectDefinition(definitionId: string) { if (definitionId === selectedId) return; requestDiscard(() => { const definition = definitions.find((item) => item.id === definitionId) ?? null; setSelectedId(definitionId); applyDefinitionDraft(definition); setError(""); setSuccess(""); }); } async function persistDraft(): Promise { if (!selected || selected.readonly || selected.status === "archived") { return selected; } setBusy(true); setError(""); try { let next = selected; if ( draft.name.trim() !== selected.name || draft.description.trim() !== (selected.description ?? "") ) { next = await updateViewDefinition(settings, selected.id, { name: draft.name.trim(), description: draft.description.trim() || null }); } if ( surfaceSetKey(draft.surfaceIds) !== surfaceSetKey(next.latest_revision.visible_surface_ids) ) { next = await createViewRevision( settings, selected.id, draft.surfaceIds ); } await load(selected.id); return next; } catch (caught) { setError(adminErrorMessage(caught)); return null; } finally { setBusy(false); } } async function saveDraft(): Promise { const next = await persistDraft(); if (!next) return false; setSuccess("View draft saved."); return true; } async function publishDraft() { const persisted = await persistDraft(); if (!persisted) return; setBusy(true); setError(""); try { await publishViewRevision( settings, persisted.id, persisted.latest_revision.id ); setSuccess(`Revision ${persisted.latest_revision.revision} published.`); await load(persisted.id); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } async function createDefinition() { setBusy(true); setError(""); try { const visibleSurfaceIds = surfaces .filter((surface) => surface.defaultVisible !== false) .map((surface) => surface.id); const created = await createViewDefinition(settings, { scope_type: scopeType, name: createDraft.name.trim(), description: createDraft.description.trim() || null, visible_surface_ids: visibleSurfaceIds }); setCreateOpen(false); setCreateDraft({ name: "", description: "" }); setSuccess("View draft created with all currently available surfaces."); await load(created.id); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } async function archiveDefinition() { if (!archiveTarget) return; setBusy(true); setError(""); try { await archiveViewDefinition(settings, archiveTarget.id); setArchiveTarget(null); setSuccess("View archived."); await load(); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } function openCreateAssignment() { const firstPublished = definitions.find( (definition) => definition.status === "published" ); setAssignmentDraft({ ...emptyAssignmentDraft(scopeType), definitionId: firstPublished?.id ?? "" }); setAssignmentEditor("new"); setError(""); } function openEditAssignment(assignment: ViewAssignment) { setAssignmentDraft({ scopeType: assignment.scope_type, scopeId: assignment.scope_id ?? "", definitionId: assignment.definition_id, mode: assignment.mode, priority: assignment.priority, active: assignment.is_active, pinRevision: Boolean(assignment.revision_id) }); setAssignmentEditor(assignment); setError(""); } async function saveAssignment() { const definition = definitions.find( (item) => item.id === assignmentDraft.definitionId ); if (!definition) return; setBusy(true); setError(""); try { const revisionId = assignmentDraft.pinRevision ? ( assignmentEditor !== "new" && assignmentEditor?.revision_id ? assignmentEditor.revision_id : definition.published_revision?.id ?? null ) : null; if (assignmentEditor === "new") { await createViewAssignment(settings, { scope_type: assignmentDraft.scopeType, scope_id: assignmentDraft.scopeId.trim() || null, definition_id: definition.id, revision_id: revisionId, mode: assignmentDraft.mode, priority: assignmentDraft.priority, is_active: assignmentDraft.active }); } else if (assignmentEditor) { await updateViewAssignment(settings, assignmentEditor.id, { revision_id: revisionId, mode: assignmentDraft.mode, priority: assignmentDraft.priority, is_active: assignmentDraft.active }); } setAssignmentEditor(null); setSuccess( assignmentEditor === "new" ? "View assignment created." : "View assignment updated." ); await load(selectedId); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } async function toggleAssignment( assignment: ViewAssignment, active: boolean ) { setBusy(true); setError(""); try { await updateViewAssignment(settings, assignment.id, { is_active: active }); await load(selectedId); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } async function removeAssignment() { if (!deleteAssignmentTarget) return; setBusy(true); setError(""); try { await deleteViewAssignment(settings, deleteAssignmentTarget.id); setDeleteAssignmentTarget(null); setSuccess("View assignment removed."); await load(selectedId); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } const title = scopeType === "system" ? "System Views" : "Tenant Views"; const description = scopeType === "system" ? "Publish reusable interface projections and assign instance-wide defaults or requirements." : "Tailor the visible interface for this tenant, its groups, and individual users."; const canCreate = canWriteDefinitions && surfaces.some((surface) => surface.kind === "navigation") && surfaces.some((surface) => surface.kind === "route"); return ( <> } onClick={() => requestDiscard(() => void load(selectedId))} disabled={loading || busy} /> } >
{selected ? ( <>

{selected.name}

{selected.readonly && ( )}

Draft revision {selected.latest_revision.revision} {selected.published_revision ? ` · published revision ${selected.published_revision.revision}` : " · not published"}

{!selected.readonly && selected.status !== "archived" && (
} variant="danger" onClick={() => setArchiveTarget(selected)} disabled={busy} />
)}
setDraft({ ...draft, name: event.target.value }) } />