import { useEffect, useState } from 'react'; import { AssessorApiClient } from '../../api/assessor-api-client'; import type { BackendStatus } from '../../app/app-state'; import { DashboardGlyph } from '../../features/dashboard/ClientTileParts'; import { EnvironmentResultsButton } from '../../features/dashboard/EnvironmentResultsButton'; import type { BsiConsistencyState, RegulatoryConsistencyItem, RegulatoryConsistencyResponse } from '../bsi-consistency'; import { useRegulatoryDecisionRevision } from './decision-revision'; import { CONSISTENCY_LABELS } from './FindingBsiRequirements'; import { useRegulatoryMapping } from './use-regulatory-mapping'; import './compliance.css'; const assessorApi = new AssessorApiClient(); export const MAPPING_REVIEW_TITLE = 'ISO ↔ BSI review'; const number = (value: number) => value.toLocaleString('en-US'); const FILTERS: Array<{ state: BsiConsistencyState; label: string }> = [ { state: 'inconsistent', label: 'Inconsistent' }, { state: 'no-reference', label: 'No ISO reference' }, { state: 'consistent', label: 'Consistent' }]; type ReviewState = { status: 'loading' } | { status: 'error' } | { status: 'ready'; review: RegulatoryConsistencyResponse }; /** * Client Info `Compliance` tab tile of the ISO <-> BSI consistency review (Phase 13F, BSI stage 2, user decision * 2026-10-09): per rule of the timeslot's deviations, the directly accepted BSI requirements against the ones the BSI * mapping derives from the accepted ISO controls. The Results window lists the rules for a person to review; a * disagreement is only shown and changes no decision by itself. */ export function MappingReviewTile({ projectId, assessmentIds, scanRunIds, backendStatus }: { projectId: string; assessmentIds: readonly string[]; scanRunIds: readonly string[]; backendStatus: BackendStatus; }) { const revision = useRegulatoryDecisionRevision(); const [state, setState] = useState({ status: 'loading' }); const [reload, setReload] = useState(0); const scopeKey = JSON.stringify([projectId, assessmentIds, scanRunIds]); useEffect(() => { let active = true; // A refresh keeps the shown values until the new review arrives; only the first load shows em dashes. setState(current => current.status === 'ready' && current.review.projectId === projectId ? current : { status: 'loading' }); assessorApi.getRegulatoryMappingConsistency(projectId, { assessmentIds: [...assessmentIds], scanRunIds: [...scanRunIds] }) .then(review => { if (active) setState({ status: 'ready', review }); }) .catch(() => { if (active) setState(current => current.status === 'ready' ? current : { status: 'error' }); }); return () => { active = false; }; }, [scopeKey, revision, reload]); // eslint-disable-line react-hooks/exhaustive-deps const ready = state.status === 'ready'; const counts = ready ? state.review.counts : undefined; const value = (amount: number | undefined) => amount === undefined ? '—' : number(amount); const compared = counts ? counts.consistent + counts.inconsistent + counts.noReference : undefined; return

{MAPPING_REVIEW_TITLE}

{ready && setReload(current => current + 1)}/> }
{state.status === 'error' ?

The ISO ↔ BSI review could not be loaded.

:
Compared rules
{value(compared)}
Inconsistent
{value(counts?.inconsistent)}
No ISO reference
{value(counts?.noReference)}
Consistent
{value(counts?.consistent)}
Not compared
{value(counts?.notCompared)}
Reviewed
{value(counts?.reviewed)}
}
; } function ReviewList({ review, projectId, backendStatus, onChanged }: { review: RegulatoryConsistencyResponse; projectId: string; backendStatus: BackendStatus; onChanged(): void; }) { const [filter, setFilter] = useState('inconsistent'); const items = review.items.filter(item => item.state === filter); return

Each rule compares the BSI requirements accepted in its direct BSI mapping with the requirements the BSI mapping {review.crosswalkVersion} derives from its accepted ISO controls. Agreement shows consistency, not correctness. Nothing here changes a decision until you accept or reject one.

{FILTERS.map(option => { const amount = review.items.filter(item => item.state === option.state).length; return ; })}
{review.counts.notCompared > 0 &&

{number(review.counts.notCompared)} {review.counts.notCompared === 1 ? 'rule has' : 'rules have'} an accepted ISO control but no accepted BSI requirement yet. Start the BSI mapping batch in the Result DB to compare them.

} {!items.length ?

No rule in this state.

:
{items.map(item => )}
}

BSI texts: BSI Stand-der-Technik-Bibliothek, catalog {review.catalogVersion.slice(0, 10)}, CC BY-SA 4.0.

; } function ReviewItem({ item, projectId, backendStatus, onChanged }: { item: RegulatoryConsistencyItem; projectId: string; backendStatus: BackendStatus; onChanged(): void; }) { const [open, setOpen] = useState(false); return
setOpen(event.currentTarget.open)}> {CONSISTENCY_LABELS[item.state]}{item.reviewed ? ' · reviewed' : ''} {item.title} {item.sourceTool}{item.sourceRuleId ? ` · ${item.sourceRuleId}` : ''} · {number(item.findingCount)} {item.findingCount === 1 ? 'finding' : 'findings'} {/* The decision hooks load only for an opened rule. */} {open && }
; } function ReviewDetail({ item, projectId, backendStatus, onChanged }: { item: RegulatoryConsistencyItem; projectId: string; backendStatus: BackendStatus; onChanged(): void; }) { const iso = useRegulatoryMapping(projectId, item.resultProjectionId, backendStatus); const bsi = useRegulatoryMapping(projectId, item.resultProjectionId, backendStatus, 'bsi-kernel'); const [comment, setComment] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const busy = saving || iso.loading || bsi.loading; const markReviewed = async () => { setSaving(true); try { await assessorApi.markRegulatoryMappingConsistencyReviewed(projectId, { resultProjectionId: item.resultProjectionId, ...(comment.trim() ? { comment: comment.trim() } : {}) }); setError(''); onChanged(); } catch (reason) { setError(reason instanceof Error ? reason.message : 'The review could not be saved.'); } finally { setSaving(false); } }; // A saved decision refreshes the tab overview and this list through the shared decision revision. return

Accepted ISO controls · {number(item.derivedCount)} derived BSI {item.derivedCount === 1 ? 'requirement' : 'requirements'}

{item.iso.length ?
    {item.iso.map(control =>
  • A.{control.controlId}{control.title ?? ''}
  • )}
:

No accepted ISO control.

}

Mapped directly to BSI

    {item.direct.map(requirement =>
  • {requirement.requirementId}{requirement.title ?? ''} {requirement.derived ? 'also derived from ISO' : requirement.reachable ? 'not derived from the accepted ISO controls' : 'no ISO control maps here'}
  • )}
{item.reverse.length > 0 &&

ISO controls the BSI mapping points to

    {item.reverse.map(control =>
  • A.{control.controlId}{control.title ?? ''} via {control.requirementIds.join(', ')} {control.decidable ? : not retrieved by the ISO mapping}
  • )}
} {(iso.error || bsi.error || error) &&

{error || iso.error || bsi.error}

} {item.state !== 'consistent' && !item.reviewed &&
}
; }