Assessor_2.0/src/features/finding-reader/use-regulatory-mapping.ts

108 lines
4.4 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import type { BackendStatus } from '../../app/app-state';
import type { FindingRecord } from '../../domain/finding';
import { AssessorApiClient } from '../../api/assessor-api-client';
import type {
RegulatoryMappingCandidate,
RegulatoryMappingDecisionValue,
RegulatoryMappingResponse
} from '../../domain/regulatory-mapping';
const assessorApi = new AssessorApiClient();
export function useRegulatoryMapping(
projectId: string,
/** Result DB row of the record: a scanner finding occurrence or a GPO baseline comparison. */
resultProjectionId: string,
backendStatus: BackendStatus
) {
const [open, setOpen] = useState(false);
const [response, setResponse] = useState<RegulatoryMappingResponse>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
setOpen(false);
setResponse(undefined);
setLoading(false);
setError('');
}, [projectId, resultProjectionId]);
const load = useCallback(async () => {
if (!projectId || !resultProjectionId || backendStatus !== 'connected') return;
setLoading(true);
try {
setResponse(await assessorApi.getRegulatoryMappings(projectId, resultProjectionId));
setError('');
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Regulatory mapping could not be loaded.');
} finally {
setLoading(false);
}
}, [backendStatus, projectId, resultProjectionId]);
useEffect(() => { if (open && !response && !loading && !error) void load(); }, [error, load, loading, open, response]);
useEffect(() => {
if (!open || !response || !['queued', 'running'].includes(response.state)) return;
const timeout = window.setTimeout(() => void load(), response.pollAfterMs ?? 1_500);
return () => window.clearTimeout(timeout);
}, [load, open, response]);
const start = useCallback(async () => {
if (!projectId || !resultProjectionId) return;
setLoading(true);
try {
setResponse(await assessorApi.generateRegulatoryMappings(projectId, { resultProjectionId }));
setError('');
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Regulatory mapping could not be started.');
} finally { setLoading(false); }
}, [projectId, resultProjectionId]);
const decide = useCallback(async (candidate: RegulatoryMappingCandidate, decision: RegulatoryMappingDecisionValue, comment?: string) => {
if (!projectId || !resultProjectionId || !response?.run) return;
const latest = response.decisions.find(item => item.controlId === candidate.controlId);
setLoading(true);
try {
setResponse(await assessorApi.saveRegulatoryMappingDecision(projectId, {
resultProjectionId,
controlId: candidate.controlId,
decision,
...(comment?.trim() ? { comment: comment.trim() } : {}),
...(latest ? { expectedLatestDecisionId: latest.id } : {})
}));
setError('');
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'The mapping decision could not be saved.');
} finally { setLoading(false); }
}, [projectId, response, resultProjectionId]);
const acceptedCount = useMemo(() => {
const latest = new Map<string, RegulatoryMappingDecisionValue>();
response?.decisions.forEach(item => { if (!latest.has(item.controlId)) latest.set(item.controlId, item.decision); });
return [...latest.values()].filter(value => value === 'accepted').length;
}, [response]);
const localDisabledReason = !resultProjectionId ? 'This record has no stored finding or current GPO baseline comparison.'
: backendStatus !== 'connected' ? 'The backend is not connected.'
: response && !response.generationEnabled ? response.disabledReason ?? 'Regulatory mapping is unavailable.' : undefined;
return {
open,
setOpen,
response,
loading,
error,
start,
decide,
acceptedCount,
disabledReason: localDisabledReason
};
}
/** Names the stored Result DB row an ISO mapping is keyed from; empty when the record has none. */
export function regulatoryMappingResultId(record: Pick<FindingRecord, 'findingOccurrenceId' | 'policyComparisonId'>): string {
if (record.findingOccurrenceId) return `scanner-finding:${record.findingOccurrenceId}`;
return record.policyComparisonId ? `policy-evaluation-comparison:${record.policyComparisonId}` : '';
}