101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, 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 [response, setResponse] = useState<RegulatoryMappingResponse>();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const resultProjectionId = occurrenceId ? `scanner-finding:${occurrenceId}` : '';
|
|
|
|
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 = !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
|
|
};
|
|
}
|