import { useCallback, useEffect, useMemo, useState } from "react"; import { AdminPageLayout, Button, Card, Dialog, FilterBar, FormField, FormGrid, MetricCard, MetricGrid, PageActionBar, SelectionList, SelectionListItem, SelectionListItemContent, StatePanel, StatusBadge, WorkspaceLayout, formatDateTime, hasScope, useUnsavedChanges, useUnsavedDraftGuard, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { createServiceDeskProfile, discoverServiceDeskProfile, listServiceDeskObjects, listServiceDeskProfiles, listServiceDeskRuns, synchronizeServiceDeskProfile, updateServiceDeskProfile, updateServiceDeskTicket, type ServiceDeskObject, type ServiceDeskProfile, type ServiceDeskRun } from "../api/externalServiceDesk"; type Props = { settings: ApiSettings; auth: AuthInfo }; type ProfileDraft = { status: "active" | "paused"; integration_mode: ServiceDeskProfile["integration_mode"]; desired_maturity: ServiceDeskProfile["desired_maturity"]; source_authority_mode: ServiceDeskProfile["source_authority_mode"]; default_visibility: ServiceDeskProfile["default_visibility"]; default_acl_tokens: string; routes: string; queue_mappings: string; dynamic_field_mappings: string; }; const DEFAULT_ROUTES = { search_path: "/Ticket/Search", ticket_path: "/Ticket/{ticket_id}", update_path: null, search_method: "POST", ticket_method: "GET", update_method: "PATCH", ticket_web_url_template: null, search_filters: {} }; const EMPTY_DRAFT: ProfileDraft = { status: "active", integration_mode: "synchronize", desired_maturity: "synchronize", source_authority_mode: "external_authoritative", default_visibility: "restricted", default_acl_tokens: "scope:connectors:service_desk:read", routes: JSON.stringify(DEFAULT_ROUTES, null, 2), queue_mappings: JSON.stringify([], null, 2), dynamic_field_mappings: JSON.stringify([], null, 2) }; export default function ExternalServiceDeskPage({ settings, auth }: Props) { const [profiles, setProfiles] = useState([]); const [objects, setObjects] = useState([]); const [runs, setRuns] = useState([]); const [selectedId, setSelectedId] = useState(""); const [draft, setDraft] = useState(EMPTY_DRAFT); const [savedKey, setSavedKey] = useState(""); const [search, setSearch] = 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 [configurationId, setConfigurationId] = useState(""); const [newDraft, setNewDraft] = useState(EMPTY_DRAFT); const [updateOpen, setUpdateOpen] = useState(false); const [updateObjectId, setUpdateObjectId] = useState(""); const [updateTitle, setUpdateTitle] = useState(""); const [updateQueue, setUpdateQueue] = useState(""); const [updateState, setUpdateState] = useState(""); const [updatePriority, setUpdatePriority] = useState(""); const [updateOwner, setUpdateOwner] = useState(""); const [updateResponsible, setUpdateResponsible] = useState(""); const [updateDynamicFields, setUpdateDynamicFields] = useState("{}"); const { requestDiscard } = useUnsavedChanges(); const selected = profiles.find((item) => item.id === selectedId) ?? null; const selectedObject = objects.find((item) => item.id === updateObjectId) ?? null; const canAdmin = hasScope(auth, "connectors:service_desk:admin"); const canSync = hasScope(auth, "connectors:service_desk:sync"); const canUpdate = hasScope(auth, "connectors:service_desk:update"); const dirty = Boolean(selected && draftKey(draft) !== savedKey); const applyProfile = useCallback((profile: ServiceDeskProfile | null) => { const next = profile ? draftFromProfile(profile) : EMPTY_DRAFT; setDraft(next); setSavedKey(profile ? draftKey(next) : ""); }, []); const reload = useCallback(async (preferredId?: string) => { setLoading(true); setError(""); try { const nextProfiles = await listServiceDeskProfiles(settings); const nextId = preferredId && nextProfiles.some((item) => item.id === preferredId) ? preferredId : nextProfiles.some((item) => item.id === selectedId) ? selectedId : nextProfiles[0]?.id ?? ""; const [nextObjects, nextRuns] = nextId ? await Promise.all([ listServiceDeskObjects(settings, nextId), listServiceDeskRuns(settings, nextId) ]) : [[], []]; setProfiles(nextProfiles); setSelectedId(nextId); setObjects(nextObjects); setRuns(nextRuns); applyProfile(nextProfiles.find((item) => item.id === nextId) ?? null); } catch (caught) { setError(errorMessage(caught)); } finally { setLoading(false); } }, [applyProfile, selectedId, settings]); useEffect(() => { void reload(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]); const save = async (): Promise => { if (!selected || !canAdmin) return false; setBusy(true); setError(""); try { const updated = await updateServiceDeskProfile(settings, selected.id, { expected_resource_revision: selected.resource_revision, status: draft.status, integration_mode: draft.integration_mode, desired_maturity: draft.desired_maturity, source_authority_mode: draft.source_authority_mode, default_visibility: draft.default_visibility, default_acl_tokens: lines(draft.default_acl_tokens), routes: parseObject(draft.routes, "Routes"), queue_mappings: parseArray(draft.queue_mappings, "Queue mappings"), dynamic_field_mappings: parseArray(draft.dynamic_field_mappings, "Dynamic-field mappings") }); setSuccess("Service-desk profile saved; queue ACLs and Search projections were refreshed."); await reload(updated.id); return true; } catch (caught) { setError(errorMessage(caught)); return false; } finally { setBusy(false); } }; useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: () => applyProfile(selected), title: "Unsaved service-desk profile changes", message: "Save or discard the profile changes before continuing." }); const selectProfile = (profile: ServiceDeskProfile) => { if (profile.id === selectedId) return; requestDiscard(() => { setSelectedId(profile.id); applyProfile(profile); setObjects([]); setRuns([]); void Promise.all([ listServiceDeskObjects(settings, profile.id), listServiceDeskRuns(settings, profile.id) ]).then(([nextObjects, nextRuns]) => { setObjects(nextObjects); setRuns(nextRuns); }).catch((caught) => setError(errorMessage(caught))); }); }; const createProfile = async () => { if (!configurationId.trim()) return; setBusy(true); setError(""); try { const created = await createServiceDeskProfile(settings, { configuration_id: configurationId.trim(), integration_mode: newDraft.integration_mode, desired_maturity: newDraft.desired_maturity, source_authority_mode: newDraft.source_authority_mode, default_visibility: newDraft.default_visibility, default_acl_tokens: lines(newDraft.default_acl_tokens), routes: parseObject(newDraft.routes, "Routes"), queue_mappings: parseArray(newDraft.queue_mappings, "Queue mappings"), dynamic_field_mappings: parseArray(newDraft.dynamic_field_mappings, "Dynamic-field mappings") }); setCreateOpen(false); setConfigurationId(""); setNewDraft(EMPTY_DRAFT); setSuccess("Service-desk profile created. Run discovery before synchronization."); await reload(created.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const discover = async () => { if (!selected || dirty) return; setBusy(true); setError(""); try { const result = await discoverServiceDeskProfile(settings, selected.id); setSuccess(`Discovered ${result.product} ${result.product_version ?? ""} at ${result.maturity} maturity with ${result.diagnostics.length} diagnostics.`); await reload(selected.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const sync = async (mode: "auto" | "full") => { if (!selected || dirty) return; setBusy(true); setError(""); try { const run = await synchronizeServiceDeskProfile(settings, selected.id, { idempotency_key: `service-desk-${mode}-${crypto.randomUUID()}`, mode, limit: 100 }); setSuccess(`${mode === "full" ? "Full synchronization" : "Next synchronization page"} completed with ${effectTotal(run)} effects.`); await reload(selected.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const openTicketUpdate = (item: ServiceDeskObject) => { setUpdateObjectId(item.id); setUpdateTitle(""); setUpdateQueue(""); setUpdateState(""); setUpdatePriority(""); setUpdateOwner(""); setUpdateResponsible(""); setUpdateDynamicFields("{}"); setUpdateOpen(true); }; const submitTicketUpdate = async () => { if (!selected || !selectedObject) return; const dynamicFields = parseObject(updateDynamicFields, "Dynamic fields"); const changes = compact({ title: updateTitle, queue: updateQueue, state: updateState, priority: updatePriority, owner: updateOwner, responsible: updateResponsible }); if (!Object.keys(changes).length && !Object.keys(dynamicFields).length) { setError("Enter at least one governed ticket change."); return; } setBusy(true); setError(""); try { const result = await updateServiceDeskTicket( settings, selected.id, selectedObject.external_id, { idempotency_key: `service-desk-update-${crypto.randomUUID()}`, expected_external_revision: selectedObject.source_revision, ...changes, dynamic_fields: dynamicFields } ); setUpdateOpen(false); setSuccess(result.outcome_unknown ? "Update outcome is unknown. Inspect the provider revision before retrying." : "Provider accepted the revision-checked ticket update and durable evidence was recorded."); await reload(selected.id); } catch (caught) { setError(errorMessage(caught)); } finally { setBusy(false); } }; const visibleProfiles = useMemo(() => { const needle = search.trim().toLocaleLowerCase(); return profiles.filter((item) => !needle || `${item.product} ${item.product_version ?? ""} ${item.integration_mode} ${item.health_status} ${item.configuration_id}` .toLocaleLowerCase().includes(needle)); }, [profiles, search]); const actionBar = void reload(selectedId), loading }} primaryActions={<> } discardAction={{ label: "Discard changes", disabled: !selected, onClick: () => applyProfile(selected) }} saveAction={{ label: "Save", disabled: !selected || !canAdmin || busy, disabledReason: !canAdmin ? "Service-desk administration permission is required." : undefined, onClick: () => void save() }} />; return item.status !== "deleted").length} /> !["healthy", "unknown"].includes(item.health_status)).length} tone="warning" /> ["failed", "outcome_unknown"].includes(item.status)).length} tone="warning" /> setSearch(event.target.value)} placeholder="Search profiles" aria-label="Search service-desk profiles" /> {visibleProfiles.map((profile) => selectProfile(profile)}> )} {!visibleProfiles.length ? : null} } > {!selected ? :
Discovered maturity: {selected.discovered_maturity} r{selected.resource_revision}