import { GitCompareArrows, History, Pencil, Plus, Send } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { FormGrid, ActionToolbar, AdminIconButton, AdminPageLayout, Button, ConfirmDialog, DataGrid, Dialog, DocumentationHelpLink, FormField, StatusBadge, TableActionGroup, ToggleSwitch, adminErrorMessage, formatAdminDateTime as formatDateTime, type ApiSettings, type DataGridColumn } from "@govoplan/core-webui"; import { approvalTemplateHistory, compareApprovalTemplateRevisions, createApprovalTemplate, listApprovalTemplates, publishApprovalTemplate, reviseApprovalTemplate, type ApprovalTemplate, type ApprovalTemplateChange, type ApprovalTemplateComparison, type ApprovalTemplateDraft } from "../../api/approvals"; import ApprovalStepsEditor, { emptyApprovalStep } from "./ApprovalStepsEditor"; import { APPROVALS_DOCUMENTATION, APPROVALS_I18N } from "./interfacePatterns"; type EditorState = { mode: "create" | "edit"; current?: ApprovalTemplate }; export default function ApprovalTemplatesPanel({ settings, canAdmin }: { settings: ApiSettings; canAdmin: boolean; }) { const [templates, setTemplates] = useState([]); const [editor, setEditor] = useState(null); const [draft, setDraft] = useState(emptyTemplate()); const [publishing, setPublishing] = useState(null); const [historyTemplate, setHistoryTemplate] = useState(null); const [history, setHistory] = useState([]); const [fromRevision, setFromRevision] = useState(1); const [toRevision, setToRevision] = useState(1); const [comparison, setComparison] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); async function load() { setLoading(true); setError(""); try { setTemplates(await listApprovalTemplates(settings)); } catch (reason) { setError(adminErrorMessage(reason)); } finally { setLoading(false); } } useEffect(() => { void load(); }, [settings.accessToken, settings.apiBaseUrl]); const columns = useMemo[]>(() => [ { id: "title", header: "Template", width: "minmax(220px, 1fr)", minWidth: 190, fill: true, sticky: "start", resizable: true, sortable: true, filterable: true, value: (row) => row.title, render: (row) =>
{row.title}
{row.key}
}, { id: "state", header: "State", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.state, render: (row) => }, { id: "revision", header: "Revision", width: 100, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.revision }, { id: "steps", header: "Steps", width: 90, resizable: false, sortable: true, value: (row) => row.steps.length }, { id: "recorded", header: "Updated", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.recorded_at, render: (row) => formatDateTime(row.recorded_at) }, { id: "actions", header: "Actions", width: 132, sticky: "end", resizable: false, align: "right", render: (row) => , disabled: !canAdmin, disabledReason: !canAdmin ? "Approval administration permission is required." : undefined, onClick: () => openEdit(row) }, { id: "history", label: `History for ${row.title}`, icon: , onClick: () => void openHistory(row) }, { id: "publish", label: `Publish ${row.title}`, icon: , applicable: row.state === "draft", disabled: !canAdmin, disabledReason: !canAdmin ? "Approval administration permission is required." : undefined, onClick: () => setPublishing(row) } ]} /> } ], [canAdmin]); const historyColumns = useMemo[]>(() => [ { id: "revision", header: "Revision", width: 90, resizable: false, value: (row) => row.revision }, { id: "state", header: "State", width: 110, resizable: false, value: (row) => row.state, render: (row) => }, { id: "recorded", header: "Recorded", width: 180, resizable: true, fill: true, value: (row) => row.recorded_at, render: (row) => formatDateTime(row.recorded_at) }, { id: "actor", header: "Actor", width: "minmax(160px, 1fr)", minWidth: 140, resizable: true, value: (row) => row.actor_id || "", render: (row) => row.actor_id || "System" }, { id: "hash", header: "Content hash", width: 180, resizable: true, value: (row) => row.content_sha256, render: (row) => {row.content_sha256.slice(0, 16)}... } ], []); const changeColumns = useMemo[]>(() => [ { id: "path", header: "Path", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, sticky: "start", value: (row) => row.path, render: (row) => {row.path} }, { id: "change", header: "Change", width: 110, resizable: false, value: (row) => row.change, render: (row) => }, { id: "before", header: "Before", width: "minmax(180px, 1fr)", minWidth: 160, fill: true, resizable: true, value: (row) => printable(row.before), render: (row) => {printable(row.before)} }, { id: "after", header: "After", width: "minmax(180px, 1fr)", minWidth: 160, fill: true, resizable: true, value: (row) => printable(row.after), render: (row) => {printable(row.after)} } ], []); function openCreate() { setDraft(emptyTemplate()); setEditor({ mode: "create" }); } function openEdit(current: ApprovalTemplate) { setDraft({ key: current.key, title: current.title, description: current.description ?? "", steps: structuredClone(current.steps), separation_of_duties: current.separation_of_duties, unique_actors_across_steps: current.unique_actors_across_steps, metadata: { ...current.metadata } }); setEditor({ mode: "edit", current }); } async function save() { if (!editor) return; setBusy(true); setError(""); try { const saved = editor.mode === "create" ? await createApprovalTemplate(settings, draft) : await reviseApprovalTemplate(settings, editor.current!, draft); setEditor(null); setSuccess(`${saved.title} saved as draft revision ${saved.revision}.`); await load(); } catch (reason) { setError(adminErrorMessage(reason)); await load(); } finally { setBusy(false); } } async function publish() { if (!publishing) return; setBusy(true); setError(""); try { const published = await publishApprovalTemplate(settings, publishing); setPublishing(null); setSuccess(`${published.title} revision ${published.revision} published.`); await load(); } catch (reason) { setError(adminErrorMessage(reason)); await load(); } finally { setBusy(false); } } async function openHistory(template: ApprovalTemplate) { setHistoryTemplate(template); setComparison(null); setError(""); try { const revisions = await approvalTemplateHistory(settings, template.id); setHistory(revisions); const newest = revisions[0]?.revision ?? template.revision; const oldest = revisions.at(-1)?.revision ?? newest; setFromRevision(oldest); setToRevision(newest); } catch (reason) { setError(adminErrorMessage(reason)); } } async function compare() { if (!historyTemplate) return; setBusy(true); setError(""); try { setComparison(await compareApprovalTemplateRevisions(settings, historyTemplate.id, fromRevision, toRevision)); } catch (reason) { setError(adminErrorMessage(reason)); } finally { setBusy(false); } } const valid = Boolean( draft.key.trim() && draft.title.trim() && draft.steps.length && draft.steps.every((step) => step.key.trim() && step.label.trim() && step.required_approvals > 0 && step.selectors.length && step.selectors.every((selector) => selector.value.trim())) ); return <> } description="Define reusable, immutable approval chains and compare every published or draft revision." loading={loading} error={error} success={success} actions={<>} variant="primary" onClick={openCreate} disabled={!canAdmin} disabledReason={!canAdmin ? "Approval administration permission is required." : undefined} />}>
row.id} emptyText="No approval templates found." />
!busy && setEditor(null)} closeDisabled={busy} className="approval-template-dialog" footer={<>}>
setDraft({ ...draft, key: event.target.value })} /> setDraft({ ...draft, title: event.target.value })} />