Migrate Risk Compliance interface patterns
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
# Risk Compliance interface pattern migration
|
||||||
|
|
||||||
|
Risk Compliance uses the platform source-evidence, governed-operation, review-queue, and revisioned-record patterns. It owns sanctions screening and assurance evidence; Connectors owns optional external source acquisition.
|
||||||
|
|
||||||
|
## Surfaces
|
||||||
|
|
||||||
|
- `risk_compliance.workspace` is the stable route surface and `risk_compliance.navigation` is its navigation entry.
|
||||||
|
- `risk_compliance.sanctions.sources` lists immutable source snapshots. `risk_compliance.action.import-snapshot` confirms copying acquired connector evidence into the normalized sanctions store.
|
||||||
|
- `risk_compliance.sanctions.screening` collects the minimum subject data needed for a version-pinned run. `risk_compliance.action.run-screening` creates durable screening evidence.
|
||||||
|
- `risk_compliance.sanctions.review` is a list-detail review queue. `risk_compliance.review.disposition` appends a legal disposition or time-bounded reusable exception.
|
||||||
|
- `risk_compliance.assurance.graph` exposes current effective assurance revisions and their bounded relationships. `risk_compliance.assurance.editor` appends object revisions and `risk_compliance.action.connect-assurance` appends typed relationships.
|
||||||
|
|
||||||
|
Backend and WebUI manifests publish the same surface identifiers and parent hierarchy so Views can reduce the workspace without private module knowledge.
|
||||||
|
|
||||||
|
## Consequences and recovery
|
||||||
|
|
||||||
|
Snapshot import is confirmed because it creates immutable normalized evidence. Importing the same content reuses the existing snapshot. A screening run pins that snapshot version and clears the transient subject draft after the backend has accepted the run.
|
||||||
|
|
||||||
|
Review dispositions are append-only. Reusable exceptions require an explicit expiry. Assurance edits preserve earlier effective-dated revisions, while system-managed sanctions projections remain read-only. A failed request leaves the draft available; navigating away from a changed screening, disposition, assurance object, or relationship invokes the shared unsaved-change guard.
|
||||||
|
|
||||||
|
Unavailable actions remain visible with selection, permission, busy-state, missing-snapshot, system-managed-record, or optional-provider reasons. Contextual help resolves through `govoplan-docs` when enabled and through the hosted fallback otherwise.
|
||||||
|
|
||||||
|
## Optional boundaries
|
||||||
|
|
||||||
|
Connectors may announce sanctions-source snapshots through its public capability. Risk Compliance does not import connector internals or credentials. Audit, Policy, Records, Files, Tasks, Notifications, Views, and Workflow integrations remain optional and communicate through declared capabilities, interfaces, and stable references.
|
||||||
@@ -336,6 +336,13 @@ DOCUMENTATION = (
|
|||||||
),
|
),
|
||||||
kind="repository",
|
kind="repository",
|
||||||
),
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Interface pattern migration",
|
||||||
|
href=(
|
||||||
|
"govoplan-risk-compliance/docs/INTERFACE_PATTERN_MIGRATION.md"
|
||||||
|
),
|
||||||
|
kind="repository",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
metadata={
|
metadata={
|
||||||
"domain_objects": [
|
"domain_objects": [
|
||||||
@@ -363,6 +370,28 @@ DOCUMENTATION = (
|
|||||||
"Every node and edge is effective-dated, revisioned, tenant-scoped, "
|
"Every node and edge is effective-dated, revisioned, tenant-scoped, "
|
||||||
"and linked through opaque governed-object references."
|
"and linked through opaque governed-object references."
|
||||||
),
|
),
|
||||||
|
"help_contexts": [
|
||||||
|
"risk_compliance.workspace",
|
||||||
|
"risk_compliance.sanctions.sources",
|
||||||
|
"risk_compliance.action.import-snapshot",
|
||||||
|
"risk_compliance.sanctions.screening",
|
||||||
|
"risk_compliance.action.run-screening",
|
||||||
|
"risk_compliance.sanctions.review",
|
||||||
|
"risk_compliance.review.disposition",
|
||||||
|
"risk_compliance.assurance.graph",
|
||||||
|
"risk_compliance.assurance.editor",
|
||||||
|
"risk_compliance.action.connect-assurance",
|
||||||
|
"risk_compliance.state.source-unavailable",
|
||||||
|
"risk_compliance.state.read-only",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"import_snapshot": "copy connector evidence into an immutable normalized sanctions-list snapshot",
|
||||||
|
"run_screening": "create immutable version-pinned screening evidence from the minimum submitted subject data",
|
||||||
|
"record_disposition": "append an evidence-backed legal review disposition that is not edited in place",
|
||||||
|
"record_exception": "append a time-bounded subject-and-entry exception with explicit expiry",
|
||||||
|
"revise_assurance_object": "append a new effective-dated revision while preserving prior evidence",
|
||||||
|
"connect_assurance_objects": "append a governed typed relationship between assurance objects",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -431,6 +460,7 @@ manifest = ModuleManifest(
|
|||||||
module_id=MODULE_ID,
|
module_id=MODULE_ID,
|
||||||
kind="section",
|
kind="section",
|
||||||
label="Sanctions source snapshots",
|
label="Sanctions source snapshots",
|
||||||
|
parent_id="risk_compliance.workspace",
|
||||||
order=20,
|
order=20,
|
||||||
),
|
),
|
||||||
ViewSurface(
|
ViewSurface(
|
||||||
@@ -438,6 +468,7 @@ manifest = ModuleManifest(
|
|||||||
module_id=MODULE_ID,
|
module_id=MODULE_ID,
|
||||||
kind="section",
|
kind="section",
|
||||||
label="Sanctions screening",
|
label="Sanctions screening",
|
||||||
|
parent_id="risk_compliance.workspace",
|
||||||
order=30,
|
order=30,
|
||||||
),
|
),
|
||||||
ViewSurface(
|
ViewSurface(
|
||||||
@@ -445,6 +476,7 @@ manifest = ModuleManifest(
|
|||||||
module_id=MODULE_ID,
|
module_id=MODULE_ID,
|
||||||
kind="section",
|
kind="section",
|
||||||
label="Sanctions review queue",
|
label="Sanctions review queue",
|
||||||
|
parent_id="risk_compliance.workspace",
|
||||||
order=40,
|
order=40,
|
||||||
),
|
),
|
||||||
ViewSurface(
|
ViewSurface(
|
||||||
@@ -452,8 +484,49 @@ manifest = ModuleManifest(
|
|||||||
module_id=MODULE_ID,
|
module_id=MODULE_ID,
|
||||||
kind="section",
|
kind="section",
|
||||||
label="Assurance graph",
|
label="Assurance graph",
|
||||||
|
parent_id="risk_compliance.workspace",
|
||||||
order=50,
|
order=50,
|
||||||
),
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="risk_compliance.action.import-snapshot",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="action",
|
||||||
|
label="Import sanctions source snapshot",
|
||||||
|
parent_id="risk_compliance.sanctions.sources",
|
||||||
|
order=60,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="risk_compliance.action.run-screening",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="action",
|
||||||
|
label="Run sanctions screening",
|
||||||
|
parent_id="risk_compliance.sanctions.screening",
|
||||||
|
order=70,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="risk_compliance.review.disposition",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="dialog",
|
||||||
|
label="Record screening disposition",
|
||||||
|
parent_id="risk_compliance.sanctions.review",
|
||||||
|
order=80,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="risk_compliance.assurance.editor",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="dialog",
|
||||||
|
label="Assurance object editor",
|
||||||
|
parent_id="risk_compliance.assurance.graph",
|
||||||
|
order=90,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="risk_compliance.action.connect-assurance",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="action",
|
||||||
|
label="Connect assurance objects",
|
||||||
|
parent_id="risk_compliance.assurance.graph",
|
||||||
|
order=100,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
tenant_summary_providers=(_tenant_summary,),
|
tenant_summary_providers=(_tenant_summary,),
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_risk_compliance.backend.manifest import get_manifest
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class RiskComplianceInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_backend_surfaces_and_hierarchy_remain_declared(self) -> None:
|
||||||
|
frontend = get_manifest().frontend
|
||||||
|
self.assertIsNotNone(frontend)
|
||||||
|
surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
|
||||||
|
expected = {
|
||||||
|
"risk_compliance.sanctions.sources",
|
||||||
|
"risk_compliance.sanctions.screening",
|
||||||
|
"risk_compliance.sanctions.review",
|
||||||
|
"risk_compliance.assurance.graph",
|
||||||
|
"risk_compliance.action.import-snapshot",
|
||||||
|
"risk_compliance.action.run-screening",
|
||||||
|
"risk_compliance.review.disposition",
|
||||||
|
"risk_compliance.assurance.editor",
|
||||||
|
"risk_compliance.action.connect-assurance",
|
||||||
|
}
|
||||||
|
self.assertEqual(expected, set(surfaces))
|
||||||
|
self.assertEqual(
|
||||||
|
"risk_compliance.workspace",
|
||||||
|
frontend.routes[0].surface_id, # type: ignore[union-attr]
|
||||||
|
)
|
||||||
|
for surface_id in (
|
||||||
|
"risk_compliance.sanctions.sources",
|
||||||
|
"risk_compliance.sanctions.screening",
|
||||||
|
"risk_compliance.sanctions.review",
|
||||||
|
"risk_compliance.assurance.graph",
|
||||||
|
):
|
||||||
|
self.assertEqual("risk_compliance.workspace", surfaces[surface_id].parent_id)
|
||||||
|
self.assertEqual(
|
||||||
|
"risk_compliance.sanctions.sources",
|
||||||
|
surfaces["risk_compliance.action.import-snapshot"].parent_id,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"risk_compliance.sanctions.screening",
|
||||||
|
surfaces["risk_compliance.action.run-screening"].parent_id,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"risk_compliance.sanctions.review",
|
||||||
|
surfaces["risk_compliance.review.disposition"].parent_id,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"risk_compliance.assurance.graph",
|
||||||
|
surfaces["risk_compliance.assurance.editor"].parent_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_help_and_consequence_metadata_remain_published(self) -> None:
|
||||||
|
topics = {topic.id: topic for topic in get_manifest().documentation}
|
||||||
|
topic = topics["risk_compliance.module-boundary"]
|
||||||
|
|
||||||
|
for context in (
|
||||||
|
"risk_compliance.action.import-snapshot",
|
||||||
|
"risk_compliance.action.run-screening",
|
||||||
|
"risk_compliance.review.disposition",
|
||||||
|
"risk_compliance.assurance.editor",
|
||||||
|
"risk_compliance.action.connect-assurance",
|
||||||
|
):
|
||||||
|
self.assertIn(context, topic.metadata["help_contexts"])
|
||||||
|
for consequence in (
|
||||||
|
"import_snapshot",
|
||||||
|
"run_screening",
|
||||||
|
"record_disposition",
|
||||||
|
"record_exception",
|
||||||
|
"revise_assurance_object",
|
||||||
|
"connect_assurance_objects",
|
||||||
|
):
|
||||||
|
self.assertIn(consequence, topic.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
def test_webui_uses_shared_governed_operation_patterns(self) -> None:
|
||||||
|
page = (
|
||||||
|
REPO_ROOT
|
||||||
|
/ "webui/src/features/riskCompliance/RiskCompliancePage.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
module = (REPO_ROOT / "webui/src/module.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for component in (
|
||||||
|
"ActionBlockerHint",
|
||||||
|
"ConfirmDialog",
|
||||||
|
"DocumentationHelpLink",
|
||||||
|
"MetricCard",
|
||||||
|
"SelectionList",
|
||||||
|
"useUnsavedDraftGuard",
|
||||||
|
):
|
||||||
|
self.assertIn(component, page)
|
||||||
|
self.assertIn("risk_compliance.assurance.graph", module)
|
||||||
|
self.assertIn("risk_compliance.action.connect-assurance", module)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -18,16 +18,24 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useSearchParams } from "react-router";
|
import { useSearchParams } from "react-router";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
IconButton,
|
IconButton,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
|
MetricCard,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
|
SelectionList,
|
||||||
|
SelectionListItem,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
i18nMessage,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type PlatformRouteContext
|
type PlatformRouteContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -55,6 +63,12 @@ import {
|
|||||||
type ReviewQueueItem,
|
type ReviewQueueItem,
|
||||||
type ScreeningRun
|
type ScreeningRun
|
||||||
} from "../../api/riskCompliance";
|
} from "../../api/riskCompliance";
|
||||||
|
import {
|
||||||
|
RISK_COMPLIANCE_ADMIN_DOCUMENTATION,
|
||||||
|
RISK_COMPLIANCE_BLOCKER_LABELS,
|
||||||
|
RISK_COMPLIANCE_DOCUMENTATION,
|
||||||
|
RISK_COMPLIANCE_I18N
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
|
|
||||||
type ViewMode = "sources" | "screen" | "review" | "assurance";
|
type ViewMode = "sources" | "screen" | "review" | "assurance";
|
||||||
@@ -93,6 +107,7 @@ export default function RiskCompliancePage({
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [notice, setNotice] = useState("");
|
const [notice, setNotice] = useState("");
|
||||||
|
const [pendingImport, setPendingImport] = useState<ConnectorSnapshot | null>(null);
|
||||||
const canAdmin = hasScope(
|
const canAdmin = hasScope(
|
||||||
auth,
|
auth,
|
||||||
"risk_compliance:sanctions:admin"
|
"risk_compliance:sanctions:admin"
|
||||||
@@ -221,6 +236,7 @@ export default function RiskCompliancePage({
|
|||||||
? `Imported ${result.snapshot.entry_count} list entries.`
|
? `Imported ${result.snapshot.entry_count} list entries.`
|
||||||
: "This immutable snapshot was already imported."
|
: "This immutable snapshot was already imported."
|
||||||
);
|
);
|
||||||
|
setPendingImport(null);
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
setError(errorMessage(reason));
|
setError(errorMessage(reason));
|
||||||
@@ -254,7 +270,8 @@ export default function RiskCompliancePage({
|
|||||||
Screen
|
Screen
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
disabled: !canScreen
|
disabled: !canScreen,
|
||||||
|
title: !canScreen ? RISK_COMPLIANCE_I18N.screenRequired : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "review",
|
id: "review",
|
||||||
@@ -267,7 +284,8 @@ export default function RiskCompliancePage({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
disabled: !canReview
|
disabled: !canReview,
|
||||||
|
title: !canReview ? RISK_COMPLIANCE_I18N.reviewRequired : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "assurance",
|
id: "assurance",
|
||||||
@@ -277,17 +295,20 @@ export default function RiskCompliancePage({
|
|||||||
Assurance
|
Assurance
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
disabled: !canReadAssurance
|
disabled: !canReadAssurance,
|
||||||
|
title: !canReadAssurance ? RISK_COMPLIANCE_I18N.assuranceReadRequired : undefined
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<span className="risk-toolbar-spacer" />
|
<span className="risk-toolbar-spacer" />
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
{loading && <LoadingIndicator size="sm" label="Loading" />}
|
{loading && <LoadingIndicator size="sm" label="Loading" />}
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Refresh"
|
label="Refresh"
|
||||||
icon={<RefreshCw size={16} />}
|
icon={<RefreshCw size={16} />}
|
||||||
onClick={() => void refresh()}
|
onClick={() => void refresh()}
|
||||||
disabled={loading || busy}
|
disabled={loading || busy}
|
||||||
|
disabledReason={loading ? RISK_COMPLIANCE_I18N.loading : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{(error || notice) && (
|
{(error || notice) && (
|
||||||
@@ -320,7 +341,7 @@ export default function RiskCompliancePage({
|
|||||||
imported={listSnapshots}
|
imported={listSnapshots}
|
||||||
canImport={canAdmin}
|
canImport={canAdmin}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
onImport={importSnapshot}
|
onImport={setPendingImport}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view === "screen" && (
|
{view === "screen" && (
|
||||||
@@ -363,6 +384,18 @@ export default function RiskCompliancePage({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={Boolean(pendingImport)}
|
||||||
|
title="i18n:govoplan-risk-compliance.import_snapshot_title"
|
||||||
|
message={i18nMessage("i18n:govoplan-risk-compliance.import_snapshot_message", {
|
||||||
|
value0: pendingImport?.publisher ?? "",
|
||||||
|
value1: pendingImport?.source_version ?? ""
|
||||||
|
})}
|
||||||
|
confirmLabel="i18n:govoplan-risk-compliance.import_snapshot"
|
||||||
|
busy={busy}
|
||||||
|
onCancel={() => setPendingImport(null)}
|
||||||
|
onConfirm={() => pendingImport && void importSnapshot(pendingImport)}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -380,7 +413,7 @@ function SourcesPane({
|
|||||||
imported: ListSnapshot[];
|
imported: ListSnapshot[];
|
||||||
canImport: boolean;
|
canImport: boolean;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
onImport: (item: ConnectorSnapshot) => Promise<void>;
|
onImport: (item: ConnectorSnapshot) => void;
|
||||||
}) {
|
}) {
|
||||||
const importedRefs = useMemo(
|
const importedRefs = useMemo(
|
||||||
() => new Set(imported.map((item) => item.sha256)),
|
() => new Set(imported.map((item) => item.sha256)),
|
||||||
@@ -394,11 +427,21 @@ function SourcesPane({
|
|||||||
<strong>Connector evidence</strong>
|
<strong>Connector evidence</strong>
|
||||||
<span>Immutable acquired source snapshots</span>
|
<span>Immutable acquired source snapshots</span>
|
||||||
</div>
|
</div>
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_ADMIN_DOCUMENTATION} />
|
||||||
</header>
|
</header>
|
||||||
{!available && (
|
{!available && (
|
||||||
<div className="risk-empty">
|
<ActionBlockerHint
|
||||||
The Connectors sanctions source capability is not enabled.
|
tone="info"
|
||||||
</div>
|
reason={{
|
||||||
|
summary: "i18n:govoplan-risk-compliance.connector_unavailable_summary",
|
||||||
|
details: RISK_COMPLIANCE_I18N.connectorUnavailable,
|
||||||
|
requiredAction: "i18n:govoplan-risk-compliance.connector_unavailable_action",
|
||||||
|
actor: "i18n:govoplan-risk-compliance.connector_unavailable_actor",
|
||||||
|
target: "i18n:govoplan-risk-compliance.connector_unavailable_target"
|
||||||
|
}}
|
||||||
|
labels={RISK_COMPLIANCE_BLOCKER_LABELS}
|
||||||
|
documentation={RISK_COMPLIANCE_ADMIN_DOCUMENTATION}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="risk-list">
|
<div className="risk-list">
|
||||||
{sources.map((item) => {
|
{sources.map((item) => {
|
||||||
@@ -419,6 +462,7 @@ function SourcesPane({
|
|||||||
label="Import snapshot"
|
label="Import snapshot"
|
||||||
icon={<Upload size={16} />}
|
icon={<Upload size={16} />}
|
||||||
disabled={!canImport || busy}
|
disabled={!canImport || busy}
|
||||||
|
disabledReason={!canImport ? RISK_COMPLIANCE_I18N.adminRequired : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||||
onClick={() => void onImport(item)}
|
onClick={() => void onImport(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -486,8 +530,31 @@ function ScreenPane({
|
|||||||
}
|
}
|
||||||
}, [listId, snapshots]);
|
}, [listId, snapshots]);
|
||||||
|
|
||||||
|
const draftDirty = Boolean(name.trim() || identifier.trim());
|
||||||
|
const submitDisabledReason = submitting
|
||||||
|
? RISK_COMPLIANCE_I18N.busy
|
||||||
|
: !listId
|
||||||
|
? RISK_COMPLIANCE_I18N.snapshotRequired
|
||||||
|
: !name.trim() && !identifier.trim()
|
||||||
|
? RISK_COMPLIANCE_I18N.subjectRequired
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: draftDirty,
|
||||||
|
onSave: executeScreening,
|
||||||
|
onDiscard: () => {
|
||||||
|
setName("");
|
||||||
|
setIdentifier("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function submit(event: FormEvent) {
|
async function submit(event: FormEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
await executeScreening();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeScreening(): Promise<boolean> {
|
||||||
|
if (submitDisabledReason) return false;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
onError("");
|
onError("");
|
||||||
try {
|
try {
|
||||||
@@ -503,8 +570,12 @@ function ScreenPane({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
onRun(response.run);
|
onRun(response.run);
|
||||||
|
setName("");
|
||||||
|
setIdentifier("");
|
||||||
|
return true;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
onError(errorMessage(reason));
|
onError(errorMessage(reason));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -518,8 +589,22 @@ function ScreenPane({
|
|||||||
<strong>New screening</strong>
|
<strong>New screening</strong>
|
||||||
<span>Use only the data needed for comparison</span>
|
<span>Use only the data needed for comparison</span>
|
||||||
</div>
|
</div>
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
</header>
|
</header>
|
||||||
<div className="risk-form-body">
|
<div className="risk-form-body">
|
||||||
|
{!snapshots.length && (
|
||||||
|
<ActionBlockerHint
|
||||||
|
reason={{
|
||||||
|
summary: "i18n:govoplan-risk-compliance.snapshot_required_summary",
|
||||||
|
details: RISK_COMPLIANCE_I18N.snapshotRequired,
|
||||||
|
requiredAction: "i18n:govoplan-risk-compliance.snapshot_required_action",
|
||||||
|
actor: "i18n:govoplan-risk-compliance.snapshot_required_actor",
|
||||||
|
target: "i18n:govoplan-risk-compliance.snapshot_required_target"
|
||||||
|
}}
|
||||||
|
labels={RISK_COMPLIANCE_BLOCKER_LABELS}
|
||||||
|
documentation={RISK_COMPLIANCE_ADMIN_DOCUMENTATION}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<FormField label="List snapshot">
|
<FormField label="List snapshot">
|
||||||
<select
|
<select
|
||||||
value={listId}
|
value={listId}
|
||||||
@@ -562,11 +647,8 @@ function ScreenPane({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={
|
disabled={Boolean(submitDisabledReason)}
|
||||||
submitting ||
|
disabledReason={submitDisabledReason}
|
||||||
!listId ||
|
|
||||||
(!name.trim() && !identifier.trim())
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Play size={16} />
|
<Play size={16} />
|
||||||
Run screening
|
Run screening
|
||||||
@@ -655,10 +737,40 @@ function ReviewPane({
|
|||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
const [reusable, setReusable] = useState(false);
|
const [reusable, setReusable] = useState(false);
|
||||||
const [expiresAt, setExpiresAt] = useState("");
|
const [expiresAt, setExpiresAt] = useState("");
|
||||||
|
const dispositionDirty = dialogOpen && Boolean(
|
||||||
|
reason.trim() || reusable || expiresAt || decision !== "false_positive"
|
||||||
|
);
|
||||||
|
const dispositionDisabledReason = busy
|
||||||
|
? RISK_COMPLIANCE_I18N.busy
|
||||||
|
: !detail
|
||||||
|
? RISK_COMPLIANCE_I18N.candidateRequired
|
||||||
|
: reason.trim().length < 3
|
||||||
|
? RISK_COMPLIANCE_I18N.dispositionReasonRequired
|
||||||
|
: reusable && !expiresAt
|
||||||
|
? RISK_COMPLIANCE_I18N.exceptionExpiryRequired
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
function resetDisposition() {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDecision("false_positive");
|
||||||
|
setReason("");
|
||||||
|
setReusable(false);
|
||||||
|
setExpiresAt("");
|
||||||
|
}
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: dispositionDirty,
|
||||||
|
onSave: recordDisposition,
|
||||||
|
onDiscard: resetDisposition
|
||||||
|
});
|
||||||
|
|
||||||
async function submitDisposition(event: FormEvent) {
|
async function submitDisposition(event: FormEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!detail) return;
|
await recordDisposition();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordDisposition(): Promise<boolean> {
|
||||||
|
if (dispositionDisabledReason || !detail) return false;
|
||||||
onBusy(true);
|
onBusy(true);
|
||||||
onError("");
|
onError("");
|
||||||
try {
|
try {
|
||||||
@@ -671,13 +783,13 @@ function ReviewPane({
|
|||||||
? new Date(`${expiresAt}T23:59:59`).toISOString()
|
? new Date(`${expiresAt}T23:59:59`).toISOString()
|
||||||
: null
|
: null
|
||||||
});
|
});
|
||||||
setDialogOpen(false);
|
resetDisposition();
|
||||||
setReason("");
|
|
||||||
setReusable(false);
|
|
||||||
onNotice("The disposition was recorded as append-only evidence.");
|
onNotice("The disposition was recorded as append-only evidence.");
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
|
return true;
|
||||||
} catch (reasonValue) {
|
} catch (reasonValue) {
|
||||||
onError(errorMessage(reasonValue));
|
onError(errorMessage(reasonValue));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
onBusy(false);
|
onBusy(false);
|
||||||
}
|
}
|
||||||
@@ -693,25 +805,25 @@ function ReviewPane({
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="risk-list">
|
<div className="risk-list">
|
||||||
{queue.map((item) => (
|
{queue.length > 0 && (
|
||||||
<button
|
<SelectionList label="Review candidates">
|
||||||
type="button"
|
{queue.map((item) => (
|
||||||
className={
|
<SelectionListItem
|
||||||
item.id === selectedId
|
className="risk-queue-row"
|
||||||
? "risk-queue-row selected"
|
selected={item.id === selectedId}
|
||||||
: "risk-queue-row"
|
key={item.id}
|
||||||
}
|
onClick={() => onSelect(item.id)}
|
||||||
key={item.id}
|
>
|
||||||
onClick={() => onSelect(item.id)}
|
<span className="risk-score">{item.score}</span>
|
||||||
>
|
<span className="risk-list-main">
|
||||||
<span className="risk-score">{item.score}</span>
|
<strong>{item.subject_name || "Identifier-only subject"}</strong>
|
||||||
<span className="risk-list-main">
|
<span>{item.entry_name}</span>
|
||||||
<strong>{item.subject_name || "Identifier-only subject"}</strong>
|
</span>
|
||||||
<span>{item.entry_name}</span>
|
<StatusBadge status={item.review_status} />
|
||||||
</span>
|
</SelectionListItem>
|
||||||
<StatusBadge status={item.review_status} />
|
))}
|
||||||
</button>
|
</SelectionList>
|
||||||
))}
|
)}
|
||||||
{!queue.length && (
|
{!queue.length && (
|
||||||
<div className="risk-empty">
|
<div className="risk-empty">
|
||||||
No candidates currently need review.
|
No candidates currently need review.
|
||||||
@@ -725,14 +837,17 @@ function ReviewPane({
|
|||||||
<strong>Candidate evidence</strong>
|
<strong>Candidate evidence</strong>
|
||||||
<span>Subject and immutable list entry comparison</span>
|
<span>Subject and immutable list entry comparison</span>
|
||||||
</div>
|
</div>
|
||||||
{detail && (
|
<div className="risk-header-actions">
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => setDialogOpen(true)}
|
onClick={() => setDialogOpen(true)}
|
||||||
|
disabled={!detail || busy}
|
||||||
|
disabledReason={!detail ? RISK_COMPLIANCE_I18N.candidateRequired : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||||
>
|
>
|
||||||
Record disposition
|
Record disposition
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{!detail && (
|
{!detail && (
|
||||||
<div className="risk-empty">
|
<div className="risk-empty">
|
||||||
@@ -788,13 +903,13 @@ function ReviewPane({
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
title="Record screening disposition"
|
title="Record screening disposition"
|
||||||
onClose={() => setDialogOpen(false)}
|
onClose={resetDisposition}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDialogOpen(false)}
|
onClick={resetDisposition}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -803,11 +918,8 @@ function ReviewPane({
|
|||||||
form="risk-disposition-form"
|
form="risk-disposition-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={
|
disabled={Boolean(dispositionDisabledReason)}
|
||||||
busy ||
|
disabledReason={dispositionDisabledReason}
|
||||||
reason.trim().length < 3 ||
|
|
||||||
(reusable && !expiresAt)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Record
|
Record
|
||||||
</Button>
|
</Button>
|
||||||
@@ -819,6 +931,19 @@ function ReviewPane({
|
|||||||
className="risk-disposition-form"
|
className="risk-disposition-form"
|
||||||
onSubmit={submitDisposition}
|
onSubmit={submitDisposition}
|
||||||
>
|
>
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
|
<ActionBlockerHint
|
||||||
|
tone="info"
|
||||||
|
reason={{
|
||||||
|
summary: "i18n:govoplan-risk-compliance.append_only_summary",
|
||||||
|
details: "i18n:govoplan-risk-compliance.append_only_details",
|
||||||
|
requiredAction: "i18n:govoplan-risk-compliance.append_only_action",
|
||||||
|
actor: "i18n:govoplan-risk-compliance.append_only_actor",
|
||||||
|
target: "i18n:govoplan-risk-compliance.append_only_target"
|
||||||
|
}}
|
||||||
|
labels={RISK_COMPLIANCE_BLOCKER_LABELS}
|
||||||
|
documentation={RISK_COMPLIANCE_DOCUMENTATION}
|
||||||
|
/>
|
||||||
<FormField label="Decision">
|
<FormField label="Decision">
|
||||||
<select
|
<select
|
||||||
value={decision}
|
value={decision}
|
||||||
@@ -950,8 +1075,10 @@ function AssurancePane({
|
|||||||
const [nodeDraft, setNodeDraft] = useState<AssuranceNodeDraft>(
|
const [nodeDraft, setNodeDraft] = useState<AssuranceNodeDraft>(
|
||||||
emptyAssuranceNodeDraft()
|
emptyAssuranceNodeDraft()
|
||||||
);
|
);
|
||||||
|
const [nodeSavedKey, setNodeSavedKey] = useState("");
|
||||||
const [edgeRelation, setEdgeRelation] = useState("");
|
const [edgeRelation, setEdgeRelation] = useState("");
|
||||||
const [edgeTarget, setEdgeTarget] = useState("");
|
const [edgeTarget, setEdgeTarget] = useState("");
|
||||||
|
const [edgeSavedKey, setEdgeSavedKey] = useState("");
|
||||||
const selected = nodes.find((item) => item.stable_id === selectedId) ?? null;
|
const selected = nodes.find((item) => item.stable_id === selectedId) ?? null;
|
||||||
const visibleNodes = useMemo(() => {
|
const visibleNodes = useMemo(() => {
|
||||||
const needle = query.trim().toLocaleLowerCase();
|
const needle = query.trim().toLocaleLowerCase();
|
||||||
@@ -973,21 +1100,63 @@ function AssurancePane({
|
|||||||
const edgeTargets = activeRelation
|
const edgeTargets = activeRelation
|
||||||
? nodes.filter((item) => item.kind === activeRelation.target)
|
? nodes.filter((item) => item.kind === activeRelation.target)
|
||||||
: [];
|
: [];
|
||||||
|
const nodeSaveDisabledReason = busy
|
||||||
|
? RISK_COMPLIANCE_I18N.busy
|
||||||
|
: !nodeDraft.stableId.trim() || !nodeDraft.label.trim() || !nodeDraft.ownerRef.trim() || !nodeDraft.validFrom || (nodeDraft.kind === "governed_object" && !nodeDraft.governedObjectRef.trim())
|
||||||
|
? RISK_COMPLIANCE_I18N.assuranceFieldsRequired
|
||||||
|
: undefined;
|
||||||
|
const edgeSaveDisabledReason = busy
|
||||||
|
? RISK_COMPLIANCE_I18N.busy
|
||||||
|
: !selected || !activeRelation || !edgeTarget
|
||||||
|
? RISK_COMPLIANCE_I18N.relationRequired
|
||||||
|
: undefined;
|
||||||
|
const nodeDirty = nodeDialogOpen && nodeDraftKey(nodeDraft) !== nodeSavedKey;
|
||||||
|
const edgeDirty = edgeDialogOpen && `${edgeRelation}:${edgeTarget}` !== edgeSavedKey;
|
||||||
|
|
||||||
|
function closeNodeDialog() {
|
||||||
|
setNodeDialogOpen(false);
|
||||||
|
setEditingNode(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEdgeDialog() {
|
||||||
|
setEdgeDialogOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: nodeDirty,
|
||||||
|
onSave: saveNode,
|
||||||
|
onDiscard: closeNodeDialog
|
||||||
|
});
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: edgeDirty,
|
||||||
|
onSave: saveEdge,
|
||||||
|
onDiscard: closeEdgeDialog
|
||||||
|
});
|
||||||
|
|
||||||
function openNewNode() {
|
function openNewNode() {
|
||||||
|
const draft = emptyAssuranceNodeDraft();
|
||||||
setEditingNode(null);
|
setEditingNode(null);
|
||||||
setNodeDraft(emptyAssuranceNodeDraft());
|
setNodeDraft(draft);
|
||||||
|
setNodeSavedKey(nodeDraftKey(draft));
|
||||||
setNodeDialogOpen(true);
|
setNodeDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditNode(item: AssuranceNode) {
|
function openEditNode(item: AssuranceNode) {
|
||||||
|
const draft = nodeDraftFromItem(item);
|
||||||
setEditingNode(item);
|
setEditingNode(item);
|
||||||
setNodeDraft(nodeDraftFromItem(item));
|
setNodeDraft(draft);
|
||||||
|
setNodeSavedKey(nodeDraftKey(draft));
|
||||||
setNodeDialogOpen(true);
|
setNodeDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitNode(event: FormEvent) {
|
async function submitNode(event: FormEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
await saveNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNode(): Promise<boolean> {
|
||||||
|
if (nodeSaveDisabledReason) return false;
|
||||||
onBusy(true);
|
onBusy(true);
|
||||||
onError("");
|
onError("");
|
||||||
try {
|
try {
|
||||||
@@ -996,7 +1165,7 @@ function AssurancePane({
|
|||||||
assuranceNodeWrite(nodeDraft, editingNode?.provenance),
|
assuranceNodeWrite(nodeDraft, editingNode?.provenance),
|
||||||
editingNode?.revision
|
editingNode?.revision
|
||||||
);
|
);
|
||||||
setNodeDialogOpen(false);
|
closeNodeDialog();
|
||||||
onSelect(saved.stable_id);
|
onSelect(saved.stable_id);
|
||||||
onNotice(
|
onNotice(
|
||||||
editingNode
|
editingNode
|
||||||
@@ -1004,8 +1173,10 @@ function AssurancePane({
|
|||||||
: "Created the assurance object."
|
: "Created the assurance object."
|
||||||
);
|
);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
|
return true;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
onError(errorMessage(reason));
|
onError(errorMessage(reason));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
onBusy(false);
|
onBusy(false);
|
||||||
}
|
}
|
||||||
@@ -1013,18 +1184,23 @@ function AssurancePane({
|
|||||||
|
|
||||||
function openEdgeDialog() {
|
function openEdgeDialog() {
|
||||||
const first = availableRelations[0];
|
const first = availableRelations[0];
|
||||||
setEdgeRelation(first?.id ?? "");
|
const relation = first?.id ?? "";
|
||||||
setEdgeTarget(
|
const target = first
|
||||||
first
|
? nodes.find((item) => item.kind === first.target)?.stable_id ?? ""
|
||||||
? nodes.find((item) => item.kind === first.target)?.stable_id ?? ""
|
: "";
|
||||||
: ""
|
setEdgeRelation(relation);
|
||||||
);
|
setEdgeTarget(target);
|
||||||
|
setEdgeSavedKey(`${relation}:${target}`);
|
||||||
setEdgeDialogOpen(true);
|
setEdgeDialogOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitEdge(event: FormEvent) {
|
async function submitEdge(event: FormEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!selected || !activeRelation || !edgeTarget) return;
|
await saveEdge();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdge(): Promise<boolean> {
|
||||||
|
if (edgeSaveDisabledReason || !selected || !activeRelation || !edgeTarget) return false;
|
||||||
onBusy(true);
|
onBusy(true);
|
||||||
onError("");
|
onError("");
|
||||||
const value: AssuranceEdgeWrite = {
|
const value: AssuranceEdgeWrite = {
|
||||||
@@ -1044,11 +1220,13 @@ function AssurancePane({
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await saveAssuranceEdge(settings, value);
|
await saveAssuranceEdge(settings, value);
|
||||||
setEdgeDialogOpen(false);
|
closeEdgeDialog();
|
||||||
onNotice("Connected the assurance objects.");
|
onNotice("Connected the assurance objects.");
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
|
return true;
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
onError(errorMessage(reason));
|
onError(errorMessage(reason));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
onBusy(false);
|
onBusy(false);
|
||||||
}
|
}
|
||||||
@@ -1056,11 +1234,31 @@ function AssurancePane({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="risk-assurance-layout">
|
<section className="risk-assurance-layout">
|
||||||
<div className="risk-assurance-metrics">
|
<div className="risk-assurance-summary">
|
||||||
<span><strong>{summary?.node_count ?? 0}</strong>objects</span>
|
{!canWrite && (
|
||||||
<span><strong>{summary?.edge_count ?? 0}</strong>relationships</span>
|
<ActionBlockerHint
|
||||||
<span><strong>{summary?.by_kind.risk ?? 0}</strong>risks</span>
|
tone="info"
|
||||||
<span><strong>{summary?.by_state.open ?? 0}</strong>open findings</span>
|
reason={{
|
||||||
|
summary: "i18n:govoplan-risk-compliance.assurance_read_only_summary",
|
||||||
|
details: RISK_COMPLIANCE_I18N.assuranceWriteRequired,
|
||||||
|
requiredAction: "i18n:govoplan-risk-compliance.permission_action",
|
||||||
|
actor: "i18n:govoplan-risk-compliance.permission_actor",
|
||||||
|
target: "i18n:govoplan-risk-compliance.permission_target"
|
||||||
|
}}
|
||||||
|
labels={RISK_COMPLIANCE_BLOCKER_LABELS}
|
||||||
|
documentation={RISK_COMPLIANCE_DOCUMENTATION}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="metric-grid risk-assurance-metrics">
|
||||||
|
<MetricCard label="Objects" value={summary?.node_count ?? 0} tone="neutral" />
|
||||||
|
<MetricCard label="Relationships" value={summary?.edge_count ?? 0} tone="neutral" />
|
||||||
|
<MetricCard label="Risks" value={summary?.by_kind.risk ?? 0} tone="warning" />
|
||||||
|
<MetricCard
|
||||||
|
label="Open findings"
|
||||||
|
value={summary?.by_state.open ?? 0}
|
||||||
|
tone={(summary?.by_state.open ?? 0) > 0 ? "danger" : "good"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="risk-assurance-columns">
|
<div className="risk-assurance-columns">
|
||||||
<aside className="risk-panel">
|
<aside className="risk-panel">
|
||||||
@@ -1069,13 +1267,16 @@ function AssurancePane({
|
|||||||
<strong>Assurance objects</strong>
|
<strong>Assurance objects</strong>
|
||||||
<span>Current effective revisions</span>
|
<span>Current effective revisions</span>
|
||||||
</div>
|
</div>
|
||||||
{canWrite && (
|
<div className="risk-header-actions">
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Add assurance object"
|
label="Add assurance object"
|
||||||
icon={<Plus size={16} />}
|
icon={<Plus size={16} />}
|
||||||
onClick={openNewNode}
|
onClick={openNewNode}
|
||||||
|
disabled={!canWrite || busy}
|
||||||
|
disabledReason={!canWrite ? RISK_COMPLIANCE_I18N.assuranceWriteRequired : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="risk-assurance-filter">
|
<div className="risk-assurance-filter">
|
||||||
<input
|
<input
|
||||||
@@ -1097,24 +1298,24 @@ function AssurancePane({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="risk-list">
|
<div className="risk-list">
|
||||||
{visibleNodes.map((item) => (
|
{visibleNodes.length > 0 && (
|
||||||
<button
|
<SelectionList label="Assurance objects">
|
||||||
type="button"
|
{visibleNodes.map((item) => (
|
||||||
className={
|
<SelectionListItem
|
||||||
item.stable_id === selectedId
|
className="risk-queue-row"
|
||||||
? "risk-queue-row selected"
|
selected={item.stable_id === selectedId}
|
||||||
: "risk-queue-row"
|
key={item.stable_id}
|
||||||
}
|
onClick={() => onSelect(item.stable_id)}
|
||||||
key={item.stable_id}
|
>
|
||||||
onClick={() => onSelect(item.stable_id)}
|
<span className="risk-list-main">
|
||||||
>
|
<strong>{item.label}</strong>
|
||||||
<span className="risk-list-main">
|
<span>{formatToken(item.kind)} · revision {item.revision}</span>
|
||||||
<strong>{item.label}</strong>
|
</span>
|
||||||
<span>{formatToken(item.kind)} · revision {item.revision}</span>
|
<StatusBadge status={item.state} />
|
||||||
</span>
|
</SelectionListItem>
|
||||||
<StatusBadge status={item.state} />
|
))}
|
||||||
</button>
|
</SelectionList>
|
||||||
))}
|
)}
|
||||||
{!visibleNodes.length && (
|
{!visibleNodes.length && (
|
||||||
<div className="risk-empty">No assurance objects match.</div>
|
<div className="risk-empty">No assurance objects match.</div>
|
||||||
)}
|
)}
|
||||||
@@ -1126,13 +1327,16 @@ function AssurancePane({
|
|||||||
<strong>{selected?.label || "Assurance object"}</strong>
|
<strong>{selected?.label || "Assurance object"}</strong>
|
||||||
<span>{selected ? formatToken(selected.kind) : "Select an object"}</span>
|
<span>{selected ? formatToken(selected.kind) : "Select an object"}</span>
|
||||||
</div>
|
</div>
|
||||||
{selected && canWrite && !selected.stable_id.startsWith("sanctions-") && (
|
<div className="risk-header-actions">
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Edit assurance object"
|
label="Edit assurance object"
|
||||||
icon={<Pencil size={16} />}
|
icon={<Pencil size={16} />}
|
||||||
onClick={() => openEditNode(selected)}
|
onClick={() => selected && openEditNode(selected)}
|
||||||
|
disabled={!selected || !canWrite || selected.stable_id.startsWith("sanctions-") || busy}
|
||||||
|
disabledReason={!selected ? RISK_COMPLIANCE_I18N.assuranceObjectRequired : !canWrite ? RISK_COMPLIANCE_I18N.assuranceWriteRequired : selected.stable_id.startsWith("sanctions-") ? RISK_COMPLIANCE_I18N.systemManagedObject : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{!selected && (
|
{!selected && (
|
||||||
<div className="risk-empty">Select an assurance object.</div>
|
<div className="risk-empty">Select an assurance object.</div>
|
||||||
@@ -1155,12 +1359,14 @@ function AssurancePane({
|
|||||||
<strong>Relationships</strong>
|
<strong>Relationships</strong>
|
||||||
<span>{graph?.edges.length ?? 0} in the bounded graph</span>
|
<span>{graph?.edges.length ?? 0} in the bounded graph</span>
|
||||||
</div>
|
</div>
|
||||||
{canWrite && availableRelations.length > 0 && (
|
<Button
|
||||||
<Button onClick={openEdgeDialog} disabled={busy}>
|
onClick={openEdgeDialog}
|
||||||
<Plus size={16} />
|
disabled={!canWrite || busy || !availableRelations.length}
|
||||||
Connect
|
disabledReason={!canWrite ? RISK_COMPLIANCE_I18N.assuranceWriteRequired : busy ? RISK_COMPLIANCE_I18N.busy : !availableRelations.length ? RISK_COMPLIANCE_I18N.relationRequired : undefined}
|
||||||
</Button>
|
>
|
||||||
)}
|
<Plus size={16} />
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="risk-assurance-links">
|
<div className="risk-assurance-links">
|
||||||
{graph?.edges.map((edge) => (
|
{graph?.edges.map((edge) => (
|
||||||
@@ -1190,29 +1396,32 @@ function AssurancePane({
|
|||||||
busy={busy}
|
busy={busy}
|
||||||
editing={Boolean(editingNode)}
|
editing={Boolean(editingNode)}
|
||||||
draft={nodeDraft}
|
draft={nodeDraft}
|
||||||
|
saveDisabledReason={nodeSaveDisabledReason}
|
||||||
onChange={setNodeDraft}
|
onChange={setNodeDraft}
|
||||||
onClose={() => setNodeDialogOpen(false)}
|
onClose={closeNodeDialog}
|
||||||
onSubmit={submitNode}
|
onSubmit={submitNode}
|
||||||
/>
|
/>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={edgeDialogOpen}
|
open={edgeDialogOpen}
|
||||||
title="Connect assurance objects"
|
title="Connect assurance objects"
|
||||||
onClose={() => setEdgeDialogOpen(false)}
|
onClose={closeEdgeDialog}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => setEdgeDialogOpen(false)} disabled={busy}>Cancel</Button>
|
<Button onClick={closeEdgeDialog} disabled={busy}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
form="risk-assurance-edge-form"
|
form="risk-assurance-edge-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={busy || !activeRelation || !edgeTarget}
|
disabled={Boolean(edgeSaveDisabledReason)}
|
||||||
|
disabledReason={edgeSaveDisabledReason}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
<form id="risk-assurance-edge-form" className="risk-assurance-form" onSubmit={submitEdge}>
|
<form id="risk-assurance-edge-form" className="risk-assurance-form" onSubmit={submitEdge}>
|
||||||
<FormField label="Relationship">
|
<FormField label="Relationship">
|
||||||
<select
|
<select
|
||||||
@@ -1275,6 +1484,7 @@ function AssuranceNodeDialog({
|
|||||||
busy,
|
busy,
|
||||||
editing,
|
editing,
|
||||||
draft,
|
draft,
|
||||||
|
saveDisabledReason,
|
||||||
onChange,
|
onChange,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit
|
onSubmit
|
||||||
@@ -1283,6 +1493,7 @@ function AssuranceNodeDialog({
|
|||||||
busy: boolean;
|
busy: boolean;
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
draft: AssuranceNodeDraft;
|
draft: AssuranceNodeDraft;
|
||||||
|
saveDisabledReason?: string;
|
||||||
onChange: (value: AssuranceNodeDraft) => void;
|
onChange: (value: AssuranceNodeDraft) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (event: FormEvent) => void;
|
onSubmit: (event: FormEvent) => void;
|
||||||
@@ -1302,13 +1513,15 @@ function AssuranceNodeDialog({
|
|||||||
form="risk-assurance-node-form"
|
form="risk-assurance-node-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={busy}
|
disabled={Boolean(saveDisabledReason)}
|
||||||
|
disabledReason={saveDisabledReason}
|
||||||
>
|
>
|
||||||
{editing ? "Record revision" : "Add"}
|
{editing ? "Record revision" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||||
<form id="risk-assurance-node-form" className="risk-assurance-form" onSubmit={onSubmit}>
|
<form id="risk-assurance-node-form" className="risk-assurance-form" onSubmit={onSubmit}>
|
||||||
<div className="risk-assurance-form-grid">
|
<div className="risk-assurance-form-grid">
|
||||||
<FormField label="Stable ID">
|
<FormField label="Stable ID">
|
||||||
@@ -1427,6 +1640,10 @@ function nodeDraftFromItem(item: AssuranceNode): AssuranceNodeDraft {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nodeDraftKey(draft: AssuranceNodeDraft) {
|
||||||
|
return JSON.stringify(draft);
|
||||||
|
}
|
||||||
|
|
||||||
function assuranceNodeWrite(
|
function assuranceNodeWrite(
|
||||||
draft: AssuranceNodeDraft,
|
draft: AssuranceNodeDraft,
|
||||||
provenance: Record<string, unknown> = { source: "risk-compliance-ui" }
|
provenance: Record<string, unknown> = { source: "risk-compliance-ui" }
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const RISK_COMPLIANCE_DOCUMENTATION = {
|
||||||
|
topicId: "risk_compliance.module-boundary",
|
||||||
|
documentationType: "user"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const RISK_COMPLIANCE_ADMIN_DOCUMENTATION = {
|
||||||
|
topicId: "risk_compliance.module-boundary",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const RISK_COMPLIANCE_I18N = {
|
||||||
|
loading: "i18n:govoplan-risk-compliance.reason.loading",
|
||||||
|
busy: "i18n:govoplan-risk-compliance.reason.busy",
|
||||||
|
adminRequired: "i18n:govoplan-risk-compliance.reason.admin_required",
|
||||||
|
screenRequired: "i18n:govoplan-risk-compliance.reason.screen_required",
|
||||||
|
reviewRequired: "i18n:govoplan-risk-compliance.reason.review_required",
|
||||||
|
assuranceReadRequired: "i18n:govoplan-risk-compliance.reason.assurance_read_required",
|
||||||
|
assuranceWriteRequired: "i18n:govoplan-risk-compliance.reason.assurance_write_required",
|
||||||
|
connectorUnavailable: "i18n:govoplan-risk-compliance.reason.connector_unavailable",
|
||||||
|
snapshotRequired: "i18n:govoplan-risk-compliance.reason.snapshot_required",
|
||||||
|
subjectRequired: "i18n:govoplan-risk-compliance.reason.subject_required",
|
||||||
|
candidateRequired: "i18n:govoplan-risk-compliance.reason.candidate_required",
|
||||||
|
dispositionReasonRequired: "i18n:govoplan-risk-compliance.reason.disposition_reason_required",
|
||||||
|
exceptionExpiryRequired: "i18n:govoplan-risk-compliance.reason.exception_expiry_required",
|
||||||
|
assuranceObjectRequired: "i18n:govoplan-risk-compliance.reason.assurance_object_required",
|
||||||
|
assuranceFieldsRequired: "i18n:govoplan-risk-compliance.reason.assurance_fields_required",
|
||||||
|
relationRequired: "i18n:govoplan-risk-compliance.reason.relation_required",
|
||||||
|
systemManagedObject: "i18n:govoplan-risk-compliance.reason.system_managed_object"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const RISK_COMPLIANCE_BLOCKER_LABELS = {
|
||||||
|
requiredAction: "i18n:govoplan-risk-compliance.blocker.required_action",
|
||||||
|
actor: "i18n:govoplan-risk-compliance.blocker.actor",
|
||||||
|
target: "i18n:govoplan-risk-compliance.blocker.target"
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const generatedTranslations: PlatformTranslations = {
|
||||||
|
en: {
|
||||||
|
"i18n:govoplan-risk-compliance.surface.sources": "Sanctions source snapshots",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.screening": "Sanctions screening",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.review": "Sanctions review queue",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.assurance": "Assurance graph",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.import_snapshot": "Import sanctions source snapshot",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.run_screening": "Run sanctions screening",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.disposition": "Record screening disposition",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.assurance_editor": "Assurance object editor",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.connect_assurance": "Connect assurance objects",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.loading": "Risk Compliance data is loading.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.busy": "A Risk Compliance operation is already running.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.admin_required": "Sanctions-administration permission is required.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.screen_required": "Sanctions-screening permission is required.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.review_required": "Sanctions-review permission is required.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_read_required": "Assurance-read permission is required.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_write_required": "Assurance-write permission is required.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.connector_unavailable": "No Connectors sanctions-source provider is enabled.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.snapshot_required": "Import and select an active list snapshot first.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.subject_required": "Enter a name or identifier to screen.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.candidate_required": "Select a candidate from the review queue first.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.disposition_reason_required": "Record a reason of at least three characters.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.exception_expiry_required": "A reusable exception requires an expiry date.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_object_required": "Select an assurance object first.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_fields_required": "Stable ID, name, owner, state, and effective-from time are required.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.relation_required": "Select a valid relationship and target object.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.system_managed_object": "Sanctions-derived assurance objects are system managed.",
|
||||||
|
"i18n:govoplan-risk-compliance.import_snapshot_title": "Import immutable snapshot?",
|
||||||
|
"i18n:govoplan-risk-compliance.import_snapshot_message": "Import {value0} source version {value1} as immutable sanctions evidence? A matching existing snapshot will be reused.",
|
||||||
|
"i18n:govoplan-risk-compliance.import_snapshot": "Import snapshot",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_summary": "Sanctions source acquisition is unavailable",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_action": "Enable a compatible Connectors sanctions-source provider, or use an already imported snapshot.",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_actor": "A module administrator",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_target": "Modules and Connectors administration",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_summary": "No sanctions snapshot is available",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_action": "Import an immutable sanctions-list snapshot before running a screening.",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_actor": "A sanctions administrator",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_target": "Risk Compliance source snapshots",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_summary": "This decision becomes durable evidence",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_details": "A disposition or reusable exception is appended to the screening record and is not edited in place.",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_action": "Verify the decision, reason, and any exception expiry before recording it.",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_actor": "The assigned compliance reviewer",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_target": "This screening candidate",
|
||||||
|
"i18n:govoplan-risk-compliance.assurance_read_only_summary": "Assurance data is read-only for this account",
|
||||||
|
"i18n:govoplan-risk-compliance.permission_action": "Request the required Risk Compliance permission from an administrator.",
|
||||||
|
"i18n:govoplan-risk-compliance.permission_actor": "A tenant access administrator",
|
||||||
|
"i18n:govoplan-risk-compliance.permission_target": "Roles and permissions",
|
||||||
|
"i18n:govoplan-risk-compliance.blocker.required_action": "Required action",
|
||||||
|
"i18n:govoplan-risk-compliance.blocker.actor": "Who can fix it",
|
||||||
|
"i18n:govoplan-risk-compliance.blocker.target": "Where to go"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
"i18n:govoplan-risk-compliance.surface.sources": "Sanktionsquellen",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.screening": "Sanktionsprüfung",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.review": "Prüfwarteschlange",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.assurance": "Assurance-Graph",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.import_snapshot": "Snapshot einer Sanktionsquelle importieren",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.run_screening": "Sanktionsprüfung ausführen",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.disposition": "Prüfentscheidung dokumentieren",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.assurance_editor": "Assurance-Objekt bearbeiten",
|
||||||
|
"i18n:govoplan-risk-compliance.surface.connect_assurance": "Assurance-Objekte verknüpfen",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.loading": "Risk-Compliance-Daten werden geladen.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.busy": "Eine Risk-Compliance-Aktion wird bereits ausgeführt.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.admin_required": "Die Berechtigung zur Verwaltung von Sanktionsprüfungen ist erforderlich.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.screen_required": "Die Berechtigung zur Sanktionsprüfung ist erforderlich.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.review_required": "Die Berechtigung zur Prüfung von Sanktionskandidaten ist erforderlich.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_read_required": "Die Berechtigung zum Lesen von Assurance-Daten ist erforderlich.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_write_required": "Die Berechtigung zum Bearbeiten von Assurance-Daten ist erforderlich.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.connector_unavailable": "Es ist kein Connectors-Anbieter für Sanktionsquellen aktiviert.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.snapshot_required": "Importieren und wählen Sie zuerst einen aktiven Listensnapshot.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.subject_required": "Geben Sie einen Namen oder eine Kennung zur Prüfung ein.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.candidate_required": "Wählen Sie zuerst einen Kandidaten aus der Prüfwarteschlange.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.disposition_reason_required": "Dokumentieren Sie eine Begründung mit mindestens drei Zeichen.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.exception_expiry_required": "Eine wiederverwendbare Ausnahme benötigt ein Ablaufdatum.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_object_required": "Wählen Sie zuerst ein Assurance-Objekt.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.assurance_fields_required": "Stabile ID, Name, zuständige Stelle, Status und Gültigkeitsbeginn sind erforderlich.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.relation_required": "Wählen Sie eine gültige Beziehung und ein Zielobjekt.",
|
||||||
|
"i18n:govoplan-risk-compliance.reason.system_managed_object": "Aus Sanktionsprüfungen abgeleitete Assurance-Objekte werden vom System verwaltet.",
|
||||||
|
"i18n:govoplan-risk-compliance.import_snapshot_title": "Unveränderlichen Snapshot importieren?",
|
||||||
|
"i18n:govoplan-risk-compliance.import_snapshot_message": "Soll {value0}, Quellversion {value1}, als unveränderlicher Sanktionsnachweis importiert werden? Ein vorhandener identischer Snapshot wird wiederverwendet.",
|
||||||
|
"i18n:govoplan-risk-compliance.import_snapshot": "Snapshot importieren",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_summary": "Sanktionsquellen können nicht abgerufen werden",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_action": "Aktivieren Sie einen kompatiblen Connectors-Anbieter für Sanktionsquellen oder verwenden Sie einen bereits importierten Snapshot.",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_actor": "Eine Moduladministration",
|
||||||
|
"i18n:govoplan-risk-compliance.connector_unavailable_target": "Modul- und Connectors-Administration",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_summary": "Es ist kein Sanktionssnapshot verfügbar",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_action": "Importieren Sie vor der Prüfung einen unveränderlichen Snapshot einer Sanktionsliste.",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_actor": "Eine Sanktionsadministration",
|
||||||
|
"i18n:govoplan-risk-compliance.snapshot_required_target": "Sanktionsquellen in Risk Compliance",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_summary": "Diese Entscheidung wird zum dauerhaften Nachweis",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_details": "Eine Prüfentscheidung oder wiederverwendbare Ausnahme wird dem Prüfdatensatz angefügt und nicht direkt geändert.",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_action": "Prüfen Sie Entscheidung, Begründung und gegebenenfalls das Ablaufdatum, bevor Sie den Eintrag anlegen.",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_actor": "Die zuständige Compliance-Prüfung",
|
||||||
|
"i18n:govoplan-risk-compliance.append_only_target": "Dieser Prüfungskandidat",
|
||||||
|
"i18n:govoplan-risk-compliance.assurance_read_only_summary": "Assurance-Daten sind für dieses Konto schreibgeschützt",
|
||||||
|
"i18n:govoplan-risk-compliance.permission_action": "Fordern Sie die erforderliche Risk-Compliance-Berechtigung bei einer Administration an.",
|
||||||
|
"i18n:govoplan-risk-compliance.permission_actor": "Eine Zugriffsadministration des Mandanten",
|
||||||
|
"i18n:govoplan-risk-compliance.permission_target": "Rollen und Berechtigungen",
|
||||||
|
"i18n:govoplan-risk-compliance.blocker.required_action": "Erforderliche Aktion",
|
||||||
|
"i18n:govoplan-risk-compliance.blocker.actor": "Zuständige Stelle",
|
||||||
|
"i18n:govoplan-risk-compliance.blocker.target": "Ziel"
|
||||||
|
}
|
||||||
|
};
|
||||||
+56
-3
@@ -1,5 +1,6 @@
|
|||||||
import { createElement, lazy } from "react";
|
import { createElement, lazy } from "react";
|
||||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
import "./styles/risk-compliance.css";
|
import "./styles/risk-compliance.css";
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export const riskComplianceModule: PlatformWebModule = {
|
|||||||
"views",
|
"views",
|
||||||
"workflow"
|
"workflow"
|
||||||
],
|
],
|
||||||
|
translations: generatedTranslations,
|
||||||
navItems: [
|
navItems: [
|
||||||
{
|
{
|
||||||
to: "/risk-compliance",
|
to: "/risk-compliance",
|
||||||
@@ -48,22 +50,73 @@ export const riskComplianceModule: PlatformWebModule = {
|
|||||||
id: "risk_compliance.sanctions.sources",
|
id: "risk_compliance.sanctions.sources",
|
||||||
moduleId: "risk_compliance",
|
moduleId: "risk_compliance",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Sanctions source snapshots",
|
label: "i18n:govoplan-risk-compliance.surface.sources",
|
||||||
|
parentId: "risk_compliance.workspace",
|
||||||
order: 20
|
order: 20
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "risk_compliance.sanctions.screening",
|
id: "risk_compliance.sanctions.screening",
|
||||||
moduleId: "risk_compliance",
|
moduleId: "risk_compliance",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Sanctions screening",
|
label: "i18n:govoplan-risk-compliance.surface.screening",
|
||||||
|
parentId: "risk_compliance.workspace",
|
||||||
order: 30
|
order: 30
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "risk_compliance.sanctions.review",
|
id: "risk_compliance.sanctions.review",
|
||||||
moduleId: "risk_compliance",
|
moduleId: "risk_compliance",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Sanctions review queue",
|
label: "i18n:govoplan-risk-compliance.surface.review",
|
||||||
|
parentId: "risk_compliance.workspace",
|
||||||
order: 40
|
order: 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "risk_compliance.assurance.graph",
|
||||||
|
moduleId: "risk_compliance",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-risk-compliance.surface.assurance",
|
||||||
|
parentId: "risk_compliance.workspace",
|
||||||
|
order: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "risk_compliance.action.import-snapshot",
|
||||||
|
moduleId: "risk_compliance",
|
||||||
|
kind: "action",
|
||||||
|
label: "i18n:govoplan-risk-compliance.surface.import_snapshot",
|
||||||
|
parentId: "risk_compliance.sanctions.sources",
|
||||||
|
order: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "risk_compliance.action.run-screening",
|
||||||
|
moduleId: "risk_compliance",
|
||||||
|
kind: "action",
|
||||||
|
label: "i18n:govoplan-risk-compliance.surface.run_screening",
|
||||||
|
parentId: "risk_compliance.sanctions.screening",
|
||||||
|
order: 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "risk_compliance.review.disposition",
|
||||||
|
moduleId: "risk_compliance",
|
||||||
|
kind: "dialog",
|
||||||
|
label: "i18n:govoplan-risk-compliance.surface.disposition",
|
||||||
|
parentId: "risk_compliance.sanctions.review",
|
||||||
|
order: 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "risk_compliance.assurance.editor",
|
||||||
|
moduleId: "risk_compliance",
|
||||||
|
kind: "dialog",
|
||||||
|
label: "i18n:govoplan-risk-compliance.surface.assurance_editor",
|
||||||
|
parentId: "risk_compliance.assurance.graph",
|
||||||
|
order: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "risk_compliance.action.connect-assurance",
|
||||||
|
moduleId: "risk_compliance",
|
||||||
|
kind: "action",
|
||||||
|
label: "i18n:govoplan-risk-compliance.surface.connect_assurance",
|
||||||
|
parentId: "risk_compliance.assurance.graph",
|
||||||
|
order: 100
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,30 +73,14 @@
|
|||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.risk-assurance-summary {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.risk-assurance-metrics {
|
.risk-assurance-metrics {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
border: var(--border-line);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.risk-assurance-metrics > span {
|
|
||||||
display: grid;
|
|
||||||
gap: 3px;
|
|
||||||
border-right: var(--border-line);
|
|
||||||
color: var(--muted);
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.risk-assurance-metrics > span:last-child {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.risk-assurance-metrics strong {
|
|
||||||
color: var(--text-strong);
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-assurance-columns {
|
.risk-assurance-columns {
|
||||||
@@ -287,6 +271,13 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.risk-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.risk-list,
|
.risk-list,
|
||||||
.risk-result-body,
|
.risk-result-body,
|
||||||
.risk-evidence-body {
|
.risk-evidence-body {
|
||||||
@@ -296,7 +287,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.risk-list-row,
|
.risk-list-row,
|
||||||
.risk-queue-row,
|
|
||||||
.risk-candidate-summary {
|
.risk-candidate-summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -310,15 +300,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.risk-queue-row {
|
.risk-queue-row {
|
||||||
width: 100%;
|
display: flex;
|
||||||
cursor: pointer;
|
min-height: 58px;
|
||||||
text-align: left;
|
align-items: center;
|
||||||
font: inherit;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-queue-row:hover,
|
.risk-list > .selection-list {
|
||||||
.risk-queue-row.selected {
|
gap: 2px;
|
||||||
background: var(--sidebar-hover-bg);
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-list-main {
|
.risk-list-main {
|
||||||
@@ -524,14 +514,6 @@
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-assurance-metrics > span:nth-child(2) {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.risk-assurance-metrics > span:nth-child(-n + 2) {
|
|
||||||
border-bottom: var(--border-line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.risk-assurance-form-grid,
|
.risk-assurance-form-grid,
|
||||||
.risk-assurance-properties {
|
.risk-assurance-properties {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
|||||||
Reference in New Issue
Block a user