import { useCallback, useEffect, useMemo, useState } from "react"; import { Archive, ArrowDown, ArrowUp, CheckSquare2, ChevronDown, ChevronRight, MinusSquare, Pencil, Plus, RefreshCw, Save, Send, Square, Trash2 } from "lucide-react"; import { FormGrid, ActionBlockerHint, AdminPageLayout, Button, ConfirmDialog, Dialog, DismissibleAlert, DocumentationHelpLink, ExplorerTree, FormField, IconButton, NavigationPreferenceEditor, SearchableSelect, SegmentedControl, SelectionList, SelectionListItem, StatusBadge, ToggleSwitch, adminErrorMessage, configurableNavigationItemsForModules, dispatchPlatformViewChanged, i18nMessage, usePlatformLanguage, usePlatformModules, useUnsavedChanges, useUnsavedDraftGuard, useViewSurfaces, type ApiSettings, type PlatformWebModule, type PlatformViewSurface, type SearchableSelectOption, type ViewPresentation } from "@govoplan/core-webui"; import { archiveViewDefinition, createViewAssignment, createViewDefinition, createViewRevision, deleteViewAssignment, fetchViewAssignmentTargets, fetchViewAssignments, fetchViewDefinitions, publishViewRevision, presentationToApi, updateViewAssignment, updateViewDefinition, type ViewAssignment, type ViewAssignmentMode, type ViewAssignmentScopeType, type ViewDefinition, type ViewScopeType } from "../../api/views"; import { VIEWS_DOCUMENTATION, VIEWS_FIELD_DOCUMENTATION, VIEWS_INTERFACE_I18N, definitionDisabledReason } from "./interfacePatterns"; type DefinitionDraft = { name: string; description: string; surfaceIds: string[]; presentation: ViewPresentation; }; type ViewProductArea = { id: string; label: string; description?: string | null; order: number; surfaceIds: string[]; }; type AssignmentDraft = { scopeType: ViewAssignmentScopeType; scopeId: string; scopeLabel: string; scopeDetail: 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, scopeId, canWriteDefinitions, canWriteAssignments, showAssignments = scopeType === "system" || scopeType === "tenant", embedded = false, title: titleOverride, description: descriptionOverride }: { settings: ApiSettings; scopeType: ViewScopeType; scopeId?: string | null; canWriteDefinitions: boolean; canWriteAssignments: boolean; showAssignments?: boolean; embedded?: boolean; title?: string; description?: string; }) { const surfaces = useViewSurfaces(); const modules = usePlatformModules(); const navigationItems = useMemo(() => configurableNavigationItemsForModules(modules), [modules]); const navigationProductAreas = useMemo(() => modules.flatMap((module) => module.productAreas ?? []), [modules]); const productAreas = useMemo(() => aggregateProductAreas(modules), [modules]); const { requestDiscard } = useUnsavedChanges(); const { translateText } = usePlatformLanguage(); const [definitions, setDefinitions] = useState([]); const [assignments, setAssignments] = useState([]); const [selectedId, setSelectedId] = useState(""); const [draft, setDraft] = useState({ name: "", description: "", surfaceIds: [], presentation: defaultPresentation([]) }); 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 [assignmentSavedDraftKey, setAssignmentSavedDraftKey] = useState(""); 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 ); const createDirty = Boolean( createOpen && (createDraft.name.trim() || createDraft.description.trim()) ); const assignmentDirty = Boolean( assignmentEditor && assignmentDraftKey(assignmentDraft) !== assignmentSavedDraftKey ); useUnsavedDraftGuard({ dirty, onSave: saveDraft, onDiscard: resetDraft }); useUnsavedDraftGuard({ dirty: createDirty, onSave: createDefinition, onDiscard: closeCreate }); useUnsavedDraftGuard({ dirty: assignmentDirty, onSave: saveAssignment, onDiscard: closeAssignment }); async function load(preferredId?: string) { setLoading(true); setError(""); try { const [nextDefinitions, nextAssignments] = await Promise.all([ fetchViewDefinitions(settings, scopeType, scopeId), showAssignments && (scopeType === "system" || scopeType === "tenant") ? fetchViewAssignments(settings, scopeType) : Promise.resolve([]) ]); 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, scopeId, showAssignments, 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, presentation: revisionPresentation( definition.latest_revision.presentation, productAreas ) } : { name: "", description: "", surfaceIds: [], presentation: defaultPresentation(productAreas) }; 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) || presentationKey(draft.presentation) !== presentationKey( revisionPresentation(next.latest_revision.presentation, productAreas) ) ) { next = await createViewRevision( settings, selected.id, draft.surfaceIds, draft.presentation ); } 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("i18n:govoplan-views.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(i18nMessage("i18n:govoplan-views.revision_published_value", { value0: persisted.latest_revision.revision })); await load(persisted.id); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } async function createDefinition(): Promise { if (!createDraft.name.trim()) return false; setBusy(true); setError(""); try { const visibleSurfaceIds = surfaces .filter((surface) => surface.defaultVisible !== false) .map((surface) => surface.id); const created = await createViewDefinition(settings, { scope_type: scopeType, scope_id: scopeId || null, name: createDraft.name.trim(), description: createDraft.description.trim() || null, visible_surface_ids: visibleSurfaceIds, presentation: presentationToApi(defaultPresentation(productAreas)) }); closeCreate(); setSuccess("i18n:govoplan-views.draft_created"); await load(created.id); return true; } catch (caught) { setError(adminErrorMessage(caught)); return false; } finally { setBusy(false); } } function closeCreate() { setCreateOpen(false); setCreateDraft({ name: "", description: "" }); } async function archiveDefinition() { if (!archiveTarget) return; setBusy(true); setError(""); try { await archiveViewDefinition(settings, archiveTarget.id); setArchiveTarget(null); setSuccess("i18n:govoplan-views.view_archived"); await load(); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } function openCreateAssignment() { const firstPublished = definitions.find( (definition) => definition.status === "published" ); const nextDraft = { ...emptyAssignmentDraft(scopeType), definitionId: firstPublished?.id ?? "" }; setAssignmentDraft(nextDraft); setAssignmentSavedDraftKey(assignmentDraftKey(nextDraft)); setAssignmentEditor("new"); setError(""); } function openEditAssignment(assignment: ViewAssignment) { const nextDraft = { scopeType: assignment.scope_type, scopeId: assignment.scope_id ?? "", scopeLabel: assignment.scope_label ?? (assignment.scope_id ? `Unavailable ${assignment.scope_type}` : assignment.scope_type), scopeDetail: assignment.scope_detail ?? (assignment.scope_label ? "" : assignment.scope_id ?? ""), definitionId: assignment.definition_id, mode: assignment.mode, priority: assignment.priority, active: assignment.is_active, pinRevision: Boolean(assignment.revision_id) }; setAssignmentDraft(nextDraft); setAssignmentSavedDraftKey(assignmentDraftKey(nextDraft)); setAssignmentEditor(assignment); setError(""); } async function saveAssignment(): Promise { const definition = definitions.find( (item) => item.id === assignmentDraft.definitionId ); if (!definition) return false; 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 }); } closeAssignment(); setSuccess( assignmentEditor === "new" ? "i18n:govoplan-views.assignment_created" : "i18n:govoplan-views.assignment_updated" ); await load(selectedId); dispatchPlatformViewChanged(); return true; } catch (caught) { setError(adminErrorMessage(caught)); return false; } finally { setBusy(false); } } function closeAssignment() { setAssignmentEditor(null); const nextDraft = emptyAssignmentDraft(scopeType); setAssignmentDraft(nextDraft); setAssignmentSavedDraftKey(assignmentDraftKey(nextDraft)); } 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("i18n:govoplan-views.assignment_removed"); await load(selectedId); dispatchPlatformViewChanged(); } catch (caught) { setError(adminErrorMessage(caught)); } finally { setBusy(false); } } const title = titleOverride ?? ({ system: "i18n:govoplan-views.system_views", tenant: "i18n:govoplan-views.tenant_views", group: "i18n:govoplan-views.group_views", user: "i18n:govoplan-views.my_views" } satisfies Record)[scopeType]; const description = descriptionOverride ?? ({ system: "i18n:govoplan-views.system_description", tenant: "i18n:govoplan-views.tenant_description", group: "i18n:govoplan-views.group_scope_description", user: "i18n:govoplan-views.user_scope_description" } satisfies Record)[scopeType]; const hasRequiredCatalogue = surfaces.some((surface) => surface.kind === "navigation") && surfaces.some((surface) => surface.kind === "route"); const canCreate = canWriteDefinitions && hasRequiredCatalogue; const definitionEditable = Boolean( selected && !selected.readonly && selected.status !== "archived" ); const reloadDisabledReason = loading ? VIEWS_INTERFACE_I18N.loadingText : busy ? VIEWS_INTERFACE_I18N.busyText : undefined; const createDisabledReason = loading ? VIEWS_INTERFACE_I18N.loadingText : busy ? VIEWS_INTERFACE_I18N.busyText : !canWriteDefinitions ? VIEWS_INTERFACE_I18N.definitionWriteText : !hasRequiredCatalogue ? VIEWS_INTERFACE_I18N.missingCatalogueText : undefined; const editDisabledReason = selected ? definitionDisabledReason({ busy, permitted: canWriteDefinitions, editable: definitionEditable, archived: selected.status === "archived" }) : undefined; return ( <> } onClick={() => requestDiscard(() => void load(selectedId))} disabled={Boolean(reloadDisabledReason)} disabledReason={reloadDisabledReason} /> } > {!canWriteDefinitions && (!showAssignments || !canWriteAssignments) && ( )}
{selected ? ( <>

{selected.name}

{selected.readonly && ( )}

{i18nMessage("i18n:govoplan-views.draft_revision_value", { value0: selected.latest_revision.revision })} · {selected.published_revision ? i18nMessage("i18n:govoplan-views.published_revision_value", { value0: selected.published_revision.revision }) : "i18n:govoplan-views.not_published"}

} variant="danger" onClick={() => setArchiveTarget(selected)} disabled={Boolean(editDisabledReason)} disabledReason={editDisabledReason} />
setDraft({ ...draft, name: event.target.value }) } />