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

119 lines
5.1 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { BackendStatus } from '../../app/app-state';
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,
occurrenceId: string | undefined,
backendStatus: BackendStatus
) {
const [open, setOpen] = useState(false);
const [storedResponse, setStoredResponse] = useState<{ scope: { key: string }; value: RegulatoryMappingResponse }>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const resultProjectionId = occurrenceId ? `scanner-finding:${occurrenceId}` : '';
const scopeKey = `${projectId}\u0000${resultProjectionId}\u0000${backendStatus}`;
const activeScope = useRef({ key: scopeKey });
if (activeScope.current.key !== scopeKey) activeScope.current = { key: scopeKey };
const requestScope = activeScope.current;
const response = storedResponse?.scope === requestScope ? storedResponse.value : undefined;
useEffect(() => {
setOpen(false);
setStoredResponse(undefined);
setLoading(false);
setError('');
}, [scopeKey]);
const load = useCallback(async () => {
if (!projectId || !resultProjectionId || backendStatus !== 'connected' || activeScope.current !== requestScope) return;
setLoading(true);
try {
const next = await assessorApi.getRegulatoryMappings(projectId, resultProjectionId);
if (activeScope.current !== requestScope) return;
setStoredResponse({ scope: requestScope, value: next });
setError('');
} catch (reason) {
if (activeScope.current !== requestScope) return;
setError(reason instanceof Error ? reason.message : 'Regulatory mapping could not be loaded.');
} finally {
if (activeScope.current === requestScope) setLoading(false);
}
}, [backendStatus, projectId, requestScope, 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 || activeScope.current !== requestScope) return;
setLoading(true);
try {
const next = await assessorApi.generateRegulatoryMappings(projectId, { resultProjectionId });
if (activeScope.current !== requestScope) return;
setStoredResponse({ scope: requestScope, value: next });
setError('');
} catch (reason) {
if (activeScope.current !== requestScope) return;
setError(reason instanceof Error ? reason.message : 'Regulatory mapping could not be started.');
} finally {
if (activeScope.current === requestScope) setLoading(false);
}
}, [projectId, requestScope, resultProjectionId]);
const decide = useCallback(async (candidate: RegulatoryMappingCandidate, decision: RegulatoryMappingDecisionValue, comment?: string) => {
if (!projectId || !resultProjectionId || !response?.run || activeScope.current !== requestScope) return;
const latest = response.decisions.find(item => item.controlId === candidate.controlId);
setLoading(true);
try {
const next = await assessorApi.saveRegulatoryMappingDecision(projectId, {
resultProjectionId,
controlId: candidate.controlId,
decision,
...(comment?.trim() ? { comment: comment.trim() } : {}),
...(latest ? { expectedLatestDecisionId: latest.id } : {})
});
if (activeScope.current !== requestScope) return;
setStoredResponse({ scope: requestScope, value: next });
setError('');
} catch (reason) {
if (activeScope.current !== requestScope) return;
setError(reason instanceof Error ? reason.message : 'The mapping decision could not be saved.');
} finally {
if (activeScope.current === requestScope) setLoading(false);
}
}, [projectId, requestScope, 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 = !occurrenceId ? 'This record has no canonical finding occurrence.'
: 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
};
}