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(); 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(); 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 }; }