Migrate Risk Compliance interface patterns
This commit is contained in:
@@ -18,16 +18,24 @@ import {
|
||||
} from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import {
|
||||
ActionBlockerHint,
|
||||
Button,
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
DocumentationHelpLink,
|
||||
FormField,
|
||||
IconButton,
|
||||
LoadingIndicator,
|
||||
MetricCard,
|
||||
SegmentedControl,
|
||||
SelectionList,
|
||||
SelectionListItem,
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
hasScope,
|
||||
i18nMessage,
|
||||
useUnsavedDraftGuard,
|
||||
type PlatformRouteContext
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
@@ -55,6 +63,12 @@ import {
|
||||
type ReviewQueueItem,
|
||||
type ScreeningRun
|
||||
} 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";
|
||||
@@ -93,6 +107,7 @@ export default function RiskCompliancePage({
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [notice, setNotice] = useState("");
|
||||
const [pendingImport, setPendingImport] = useState<ConnectorSnapshot | null>(null);
|
||||
const canAdmin = hasScope(
|
||||
auth,
|
||||
"risk_compliance:sanctions:admin"
|
||||
@@ -221,6 +236,7 @@ export default function RiskCompliancePage({
|
||||
? `Imported ${result.snapshot.entry_count} list entries.`
|
||||
: "This immutable snapshot was already imported."
|
||||
);
|
||||
setPendingImport(null);
|
||||
await refresh();
|
||||
} catch (reason) {
|
||||
setError(errorMessage(reason));
|
||||
@@ -254,7 +270,8 @@ export default function RiskCompliancePage({
|
||||
Screen
|
||||
</>
|
||||
),
|
||||
disabled: !canScreen
|
||||
disabled: !canScreen,
|
||||
title: !canScreen ? RISK_COMPLIANCE_I18N.screenRequired : undefined
|
||||
},
|
||||
{
|
||||
id: "review",
|
||||
@@ -267,7 +284,8 @@ export default function RiskCompliancePage({
|
||||
)}
|
||||
</>
|
||||
),
|
||||
disabled: !canReview
|
||||
disabled: !canReview,
|
||||
title: !canReview ? RISK_COMPLIANCE_I18N.reviewRequired : undefined
|
||||
},
|
||||
{
|
||||
id: "assurance",
|
||||
@@ -277,17 +295,20 @@ export default function RiskCompliancePage({
|
||||
Assurance
|
||||
</>
|
||||
),
|
||||
disabled: !canReadAssurance
|
||||
disabled: !canReadAssurance,
|
||||
title: !canReadAssurance ? RISK_COMPLIANCE_I18N.assuranceReadRequired : undefined
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<span className="risk-toolbar-spacer" />
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||
{loading && <LoadingIndicator size="sm" label="Loading" />}
|
||||
<IconButton
|
||||
label="Refresh"
|
||||
icon={<RefreshCw size={16} />}
|
||||
onClick={() => void refresh()}
|
||||
disabled={loading || busy}
|
||||
disabledReason={loading ? RISK_COMPLIANCE_I18N.loading : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||
/>
|
||||
</div>
|
||||
{(error || notice) && (
|
||||
@@ -320,7 +341,7 @@ export default function RiskCompliancePage({
|
||||
imported={listSnapshots}
|
||||
canImport={canAdmin}
|
||||
busy={busy}
|
||||
onImport={importSnapshot}
|
||||
onImport={setPendingImport}
|
||||
/>
|
||||
)}
|
||||
{view === "screen" && (
|
||||
@@ -363,6 +384,18 @@ export default function RiskCompliancePage({
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -380,7 +413,7 @@ function SourcesPane({
|
||||
imported: ListSnapshot[];
|
||||
canImport: boolean;
|
||||
busy: boolean;
|
||||
onImport: (item: ConnectorSnapshot) => Promise<void>;
|
||||
onImport: (item: ConnectorSnapshot) => void;
|
||||
}) {
|
||||
const importedRefs = useMemo(
|
||||
() => new Set(imported.map((item) => item.sha256)),
|
||||
@@ -394,11 +427,21 @@ function SourcesPane({
|
||||
<strong>Connector evidence</strong>
|
||||
<span>Immutable acquired source snapshots</span>
|
||||
</div>
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_ADMIN_DOCUMENTATION} />
|
||||
</header>
|
||||
{!available && (
|
||||
<div className="risk-empty">
|
||||
The Connectors sanctions source capability is not enabled.
|
||||
</div>
|
||||
<ActionBlockerHint
|
||||
tone="info"
|
||||
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">
|
||||
{sources.map((item) => {
|
||||
@@ -419,6 +462,7 @@ function SourcesPane({
|
||||
label="Import snapshot"
|
||||
icon={<Upload size={16} />}
|
||||
disabled={!canImport || busy}
|
||||
disabledReason={!canImport ? RISK_COMPLIANCE_I18N.adminRequired : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||
onClick={() => void onImport(item)}
|
||||
/>
|
||||
)}
|
||||
@@ -486,8 +530,31 @@ function ScreenPane({
|
||||
}
|
||||
}, [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) {
|
||||
event.preventDefault();
|
||||
await executeScreening();
|
||||
}
|
||||
|
||||
async function executeScreening(): Promise<boolean> {
|
||||
if (submitDisabledReason) return false;
|
||||
setSubmitting(true);
|
||||
onError("");
|
||||
try {
|
||||
@@ -503,8 +570,12 @@ function ScreenPane({
|
||||
}
|
||||
});
|
||||
onRun(response.run);
|
||||
setName("");
|
||||
setIdentifier("");
|
||||
return true;
|
||||
} catch (reason) {
|
||||
onError(errorMessage(reason));
|
||||
return false;
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -518,8 +589,22 @@ function ScreenPane({
|
||||
<strong>New screening</strong>
|
||||
<span>Use only the data needed for comparison</span>
|
||||
</div>
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||
</header>
|
||||
<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">
|
||||
<select
|
||||
value={listId}
|
||||
@@ -562,11 +647,8 @@ function ScreenPane({
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={
|
||||
submitting ||
|
||||
!listId ||
|
||||
(!name.trim() && !identifier.trim())
|
||||
}
|
||||
disabled={Boolean(submitDisabledReason)}
|
||||
disabledReason={submitDisabledReason}
|
||||
>
|
||||
<Play size={16} />
|
||||
Run screening
|
||||
@@ -655,10 +737,40 @@ function ReviewPane({
|
||||
const [reason, setReason] = useState("");
|
||||
const [reusable, setReusable] = useState(false);
|
||||
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) {
|
||||
event.preventDefault();
|
||||
if (!detail) return;
|
||||
await recordDisposition();
|
||||
}
|
||||
|
||||
async function recordDisposition(): Promise<boolean> {
|
||||
if (dispositionDisabledReason || !detail) return false;
|
||||
onBusy(true);
|
||||
onError("");
|
||||
try {
|
||||
@@ -671,13 +783,13 @@ function ReviewPane({
|
||||
? new Date(`${expiresAt}T23:59:59`).toISOString()
|
||||
: null
|
||||
});
|
||||
setDialogOpen(false);
|
||||
setReason("");
|
||||
setReusable(false);
|
||||
resetDisposition();
|
||||
onNotice("The disposition was recorded as append-only evidence.");
|
||||
await onRefresh();
|
||||
return true;
|
||||
} catch (reasonValue) {
|
||||
onError(errorMessage(reasonValue));
|
||||
return false;
|
||||
} finally {
|
||||
onBusy(false);
|
||||
}
|
||||
@@ -693,25 +805,25 @@ function ReviewPane({
|
||||
</div>
|
||||
</header>
|
||||
<div className="risk-list">
|
||||
{queue.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
item.id === selectedId
|
||||
? "risk-queue-row selected"
|
||||
: "risk-queue-row"
|
||||
}
|
||||
key={item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
>
|
||||
<span className="risk-score">{item.score}</span>
|
||||
<span className="risk-list-main">
|
||||
<strong>{item.subject_name || "Identifier-only subject"}</strong>
|
||||
<span>{item.entry_name}</span>
|
||||
</span>
|
||||
<StatusBadge status={item.review_status} />
|
||||
</button>
|
||||
))}
|
||||
{queue.length > 0 && (
|
||||
<SelectionList label="Review candidates">
|
||||
{queue.map((item) => (
|
||||
<SelectionListItem
|
||||
className="risk-queue-row"
|
||||
selected={item.id === selectedId}
|
||||
key={item.id}
|
||||
onClick={() => onSelect(item.id)}
|
||||
>
|
||||
<span className="risk-score">{item.score}</span>
|
||||
<span className="risk-list-main">
|
||||
<strong>{item.subject_name || "Identifier-only subject"}</strong>
|
||||
<span>{item.entry_name}</span>
|
||||
</span>
|
||||
<StatusBadge status={item.review_status} />
|
||||
</SelectionListItem>
|
||||
))}
|
||||
</SelectionList>
|
||||
)}
|
||||
{!queue.length && (
|
||||
<div className="risk-empty">
|
||||
No candidates currently need review.
|
||||
@@ -725,14 +837,17 @@ function ReviewPane({
|
||||
<strong>Candidate evidence</strong>
|
||||
<span>Subject and immutable list entry comparison</span>
|
||||
</div>
|
||||
{detail && (
|
||||
<div className="risk-header-actions">
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
disabled={!detail || busy}
|
||||
disabledReason={!detail ? RISK_COMPLIANCE_I18N.candidateRequired : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||
>
|
||||
Record disposition
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
{!detail && (
|
||||
<div className="risk-empty">
|
||||
@@ -788,13 +903,13 @@ function ReviewPane({
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
title="Record screening disposition"
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onClose={resetDisposition}
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
onClick={resetDisposition}
|
||||
disabled={busy}
|
||||
>
|
||||
Cancel
|
||||
@@ -803,11 +918,8 @@ function ReviewPane({
|
||||
form="risk-disposition-form"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={
|
||||
busy ||
|
||||
reason.trim().length < 3 ||
|
||||
(reusable && !expiresAt)
|
||||
}
|
||||
disabled={Boolean(dispositionDisabledReason)}
|
||||
disabledReason={dispositionDisabledReason}
|
||||
>
|
||||
Record
|
||||
</Button>
|
||||
@@ -819,6 +931,19 @@ function ReviewPane({
|
||||
className="risk-disposition-form"
|
||||
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">
|
||||
<select
|
||||
value={decision}
|
||||
@@ -950,8 +1075,10 @@ function AssurancePane({
|
||||
const [nodeDraft, setNodeDraft] = useState<AssuranceNodeDraft>(
|
||||
emptyAssuranceNodeDraft()
|
||||
);
|
||||
const [nodeSavedKey, setNodeSavedKey] = useState("");
|
||||
const [edgeRelation, setEdgeRelation] = useState("");
|
||||
const [edgeTarget, setEdgeTarget] = useState("");
|
||||
const [edgeSavedKey, setEdgeSavedKey] = useState("");
|
||||
const selected = nodes.find((item) => item.stable_id === selectedId) ?? null;
|
||||
const visibleNodes = useMemo(() => {
|
||||
const needle = query.trim().toLocaleLowerCase();
|
||||
@@ -973,21 +1100,63 @@ function AssurancePane({
|
||||
const edgeTargets = activeRelation
|
||||
? 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() {
|
||||
const draft = emptyAssuranceNodeDraft();
|
||||
setEditingNode(null);
|
||||
setNodeDraft(emptyAssuranceNodeDraft());
|
||||
setNodeDraft(draft);
|
||||
setNodeSavedKey(nodeDraftKey(draft));
|
||||
setNodeDialogOpen(true);
|
||||
}
|
||||
|
||||
function openEditNode(item: AssuranceNode) {
|
||||
const draft = nodeDraftFromItem(item);
|
||||
setEditingNode(item);
|
||||
setNodeDraft(nodeDraftFromItem(item));
|
||||
setNodeDraft(draft);
|
||||
setNodeSavedKey(nodeDraftKey(draft));
|
||||
setNodeDialogOpen(true);
|
||||
}
|
||||
|
||||
async function submitNode(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
await saveNode();
|
||||
}
|
||||
|
||||
async function saveNode(): Promise<boolean> {
|
||||
if (nodeSaveDisabledReason) return false;
|
||||
onBusy(true);
|
||||
onError("");
|
||||
try {
|
||||
@@ -996,7 +1165,7 @@ function AssurancePane({
|
||||
assuranceNodeWrite(nodeDraft, editingNode?.provenance),
|
||||
editingNode?.revision
|
||||
);
|
||||
setNodeDialogOpen(false);
|
||||
closeNodeDialog();
|
||||
onSelect(saved.stable_id);
|
||||
onNotice(
|
||||
editingNode
|
||||
@@ -1004,8 +1173,10 @@ function AssurancePane({
|
||||
: "Created the assurance object."
|
||||
);
|
||||
await onRefresh();
|
||||
return true;
|
||||
} catch (reason) {
|
||||
onError(errorMessage(reason));
|
||||
return false;
|
||||
} finally {
|
||||
onBusy(false);
|
||||
}
|
||||
@@ -1013,18 +1184,23 @@ function AssurancePane({
|
||||
|
||||
function openEdgeDialog() {
|
||||
const first = availableRelations[0];
|
||||
setEdgeRelation(first?.id ?? "");
|
||||
setEdgeTarget(
|
||||
first
|
||||
? nodes.find((item) => item.kind === first.target)?.stable_id ?? ""
|
||||
: ""
|
||||
);
|
||||
const relation = first?.id ?? "";
|
||||
const target = first
|
||||
? nodes.find((item) => item.kind === first.target)?.stable_id ?? ""
|
||||
: "";
|
||||
setEdgeRelation(relation);
|
||||
setEdgeTarget(target);
|
||||
setEdgeSavedKey(`${relation}:${target}`);
|
||||
setEdgeDialogOpen(true);
|
||||
}
|
||||
|
||||
async function submitEdge(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (!selected || !activeRelation || !edgeTarget) return;
|
||||
await saveEdge();
|
||||
}
|
||||
|
||||
async function saveEdge(): Promise<boolean> {
|
||||
if (edgeSaveDisabledReason || !selected || !activeRelation || !edgeTarget) return false;
|
||||
onBusy(true);
|
||||
onError("");
|
||||
const value: AssuranceEdgeWrite = {
|
||||
@@ -1044,11 +1220,13 @@ function AssurancePane({
|
||||
};
|
||||
try {
|
||||
await saveAssuranceEdge(settings, value);
|
||||
setEdgeDialogOpen(false);
|
||||
closeEdgeDialog();
|
||||
onNotice("Connected the assurance objects.");
|
||||
await onRefresh();
|
||||
return true;
|
||||
} catch (reason) {
|
||||
onError(errorMessage(reason));
|
||||
return false;
|
||||
} finally {
|
||||
onBusy(false);
|
||||
}
|
||||
@@ -1056,11 +1234,31 @@ function AssurancePane({
|
||||
|
||||
return (
|
||||
<section className="risk-assurance-layout">
|
||||
<div className="risk-assurance-metrics">
|
||||
<span><strong>{summary?.node_count ?? 0}</strong>objects</span>
|
||||
<span><strong>{summary?.edge_count ?? 0}</strong>relationships</span>
|
||||
<span><strong>{summary?.by_kind.risk ?? 0}</strong>risks</span>
|
||||
<span><strong>{summary?.by_state.open ?? 0}</strong>open findings</span>
|
||||
<div className="risk-assurance-summary">
|
||||
{!canWrite && (
|
||||
<ActionBlockerHint
|
||||
tone="info"
|
||||
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 className="risk-assurance-columns">
|
||||
<aside className="risk-panel">
|
||||
@@ -1069,13 +1267,16 @@ function AssurancePane({
|
||||
<strong>Assurance objects</strong>
|
||||
<span>Current effective revisions</span>
|
||||
</div>
|
||||
{canWrite && (
|
||||
<div className="risk-header-actions">
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||
<IconButton
|
||||
label="Add assurance object"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={openNewNode}
|
||||
disabled={!canWrite || busy}
|
||||
disabledReason={!canWrite ? RISK_COMPLIANCE_I18N.assuranceWriteRequired : busy ? RISK_COMPLIANCE_I18N.busy : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div className="risk-assurance-filter">
|
||||
<input
|
||||
@@ -1097,24 +1298,24 @@ function AssurancePane({
|
||||
</select>
|
||||
</div>
|
||||
<div className="risk-list">
|
||||
{visibleNodes.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
item.stable_id === selectedId
|
||||
? "risk-queue-row selected"
|
||||
: "risk-queue-row"
|
||||
}
|
||||
key={item.stable_id}
|
||||
onClick={() => onSelect(item.stable_id)}
|
||||
>
|
||||
<span className="risk-list-main">
|
||||
<strong>{item.label}</strong>
|
||||
<span>{formatToken(item.kind)} · revision {item.revision}</span>
|
||||
</span>
|
||||
<StatusBadge status={item.state} />
|
||||
</button>
|
||||
))}
|
||||
{visibleNodes.length > 0 && (
|
||||
<SelectionList label="Assurance objects">
|
||||
{visibleNodes.map((item) => (
|
||||
<SelectionListItem
|
||||
className="risk-queue-row"
|
||||
selected={item.stable_id === selectedId}
|
||||
key={item.stable_id}
|
||||
onClick={() => onSelect(item.stable_id)}
|
||||
>
|
||||
<span className="risk-list-main">
|
||||
<strong>{item.label}</strong>
|
||||
<span>{formatToken(item.kind)} · revision {item.revision}</span>
|
||||
</span>
|
||||
<StatusBadge status={item.state} />
|
||||
</SelectionListItem>
|
||||
))}
|
||||
</SelectionList>
|
||||
)}
|
||||
{!visibleNodes.length && (
|
||||
<div className="risk-empty">No assurance objects match.</div>
|
||||
)}
|
||||
@@ -1126,13 +1327,16 @@ function AssurancePane({
|
||||
<strong>{selected?.label || "Assurance object"}</strong>
|
||||
<span>{selected ? formatToken(selected.kind) : "Select an object"}</span>
|
||||
</div>
|
||||
{selected && canWrite && !selected.stable_id.startsWith("sanctions-") && (
|
||||
<div className="risk-header-actions">
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||
<IconButton
|
||||
label="Edit assurance object"
|
||||
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>
|
||||
{!selected && (
|
||||
<div className="risk-empty">Select an assurance object.</div>
|
||||
@@ -1155,12 +1359,14 @@ function AssurancePane({
|
||||
<strong>Relationships</strong>
|
||||
<span>{graph?.edges.length ?? 0} in the bounded graph</span>
|
||||
</div>
|
||||
{canWrite && availableRelations.length > 0 && (
|
||||
<Button onClick={openEdgeDialog} disabled={busy}>
|
||||
<Plus size={16} />
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={openEdgeDialog}
|
||||
disabled={!canWrite || busy || !availableRelations.length}
|
||||
disabledReason={!canWrite ? RISK_COMPLIANCE_I18N.assuranceWriteRequired : busy ? RISK_COMPLIANCE_I18N.busy : !availableRelations.length ? RISK_COMPLIANCE_I18N.relationRequired : undefined}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
<div className="risk-assurance-links">
|
||||
{graph?.edges.map((edge) => (
|
||||
@@ -1190,29 +1396,32 @@ function AssurancePane({
|
||||
busy={busy}
|
||||
editing={Boolean(editingNode)}
|
||||
draft={nodeDraft}
|
||||
saveDisabledReason={nodeSaveDisabledReason}
|
||||
onChange={setNodeDraft}
|
||||
onClose={() => setNodeDialogOpen(false)}
|
||||
onClose={closeNodeDialog}
|
||||
onSubmit={submitNode}
|
||||
/>
|
||||
<Dialog
|
||||
open={edgeDialogOpen}
|
||||
title="Connect assurance objects"
|
||||
onClose={() => setEdgeDialogOpen(false)}
|
||||
onClose={closeEdgeDialog}
|
||||
closeDisabled={busy}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => setEdgeDialogOpen(false)} disabled={busy}>Cancel</Button>
|
||||
<Button onClick={closeEdgeDialog} disabled={busy}>Cancel</Button>
|
||||
<Button
|
||||
form="risk-assurance-edge-form"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={busy || !activeRelation || !edgeTarget}
|
||||
disabled={Boolean(edgeSaveDisabledReason)}
|
||||
disabledReason={edgeSaveDisabledReason}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||
<form id="risk-assurance-edge-form" className="risk-assurance-form" onSubmit={submitEdge}>
|
||||
<FormField label="Relationship">
|
||||
<select
|
||||
@@ -1275,6 +1484,7 @@ function AssuranceNodeDialog({
|
||||
busy,
|
||||
editing,
|
||||
draft,
|
||||
saveDisabledReason,
|
||||
onChange,
|
||||
onClose,
|
||||
onSubmit
|
||||
@@ -1283,6 +1493,7 @@ function AssuranceNodeDialog({
|
||||
busy: boolean;
|
||||
editing: boolean;
|
||||
draft: AssuranceNodeDraft;
|
||||
saveDisabledReason?: string;
|
||||
onChange: (value: AssuranceNodeDraft) => void;
|
||||
onClose: () => void;
|
||||
onSubmit: (event: FormEvent) => void;
|
||||
@@ -1302,13 +1513,15 @@ function AssuranceNodeDialog({
|
||||
form="risk-assurance-node-form"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={busy}
|
||||
disabled={Boolean(saveDisabledReason)}
|
||||
disabledReason={saveDisabledReason}
|
||||
>
|
||||
{editing ? "Record revision" : "Add"}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<DocumentationHelpLink reference={RISK_COMPLIANCE_DOCUMENTATION} />
|
||||
<form id="risk-assurance-node-form" className="risk-assurance-form" onSubmit={onSubmit}>
|
||||
<div className="risk-assurance-form-grid">
|
||||
<FormField label="Stable ID">
|
||||
@@ -1427,6 +1640,10 @@ function nodeDraftFromItem(item: AssuranceNode): AssuranceNodeDraft {
|
||||
};
|
||||
}
|
||||
|
||||
function nodeDraftKey(draft: AssuranceNodeDraft) {
|
||||
return JSON.stringify(draft);
|
||||
}
|
||||
|
||||
function assuranceNodeWrite(
|
||||
draft: AssuranceNodeDraft,
|
||||
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 type { PlatformWebModule } from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import "./styles/risk-compliance.css";
|
||||
|
||||
|
||||
@@ -24,6 +25,7 @@ export const riskComplianceModule: PlatformWebModule = {
|
||||
"views",
|
||||
"workflow"
|
||||
],
|
||||
translations: generatedTranslations,
|
||||
navItems: [
|
||||
{
|
||||
to: "/risk-compliance",
|
||||
@@ -48,22 +50,73 @@ export const riskComplianceModule: PlatformWebModule = {
|
||||
id: "risk_compliance.sanctions.sources",
|
||||
moduleId: "risk_compliance",
|
||||
kind: "section",
|
||||
label: "Sanctions source snapshots",
|
||||
label: "i18n:govoplan-risk-compliance.surface.sources",
|
||||
parentId: "risk_compliance.workspace",
|
||||
order: 20
|
||||
},
|
||||
{
|
||||
id: "risk_compliance.sanctions.screening",
|
||||
moduleId: "risk_compliance",
|
||||
kind: "section",
|
||||
label: "Sanctions screening",
|
||||
label: "i18n:govoplan-risk-compliance.surface.screening",
|
||||
parentId: "risk_compliance.workspace",
|
||||
order: 30
|
||||
},
|
||||
{
|
||||
id: "risk_compliance.sanctions.review",
|
||||
moduleId: "risk_compliance",
|
||||
kind: "section",
|
||||
label: "Sanctions review queue",
|
||||
label: "i18n:govoplan-risk-compliance.surface.review",
|
||||
parentId: "risk_compliance.workspace",
|
||||
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);
|
||||
}
|
||||
|
||||
.risk-assurance-summary {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.risk-assurance-metrics {
|
||||
display: grid;
|
||||
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 {
|
||||
@@ -287,6 +271,13 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.risk-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.risk-list,
|
||||
.risk-result-body,
|
||||
.risk-evidence-body {
|
||||
@@ -296,7 +287,6 @@
|
||||
}
|
||||
|
||||
.risk-list-row,
|
||||
.risk-queue-row,
|
||||
.risk-candidate-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -310,15 +300,15 @@
|
||||
}
|
||||
|
||||
.risk-queue-row {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
display: flex;
|
||||
min-height: 58px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.risk-queue-row:hover,
|
||||
.risk-queue-row.selected {
|
||||
background: var(--sidebar-hover-bg);
|
||||
.risk-list > .selection-list {
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.risk-list-main {
|
||||
@@ -524,14 +514,6 @@
|
||||
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-properties {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user