import { useCallback, useEffect, useRef, useState } from 'react'; import { AssessorApiClient } from '../../api/assessor-api-client'; import { Dialog } from '../../components/Dialog/Dialog'; import type { RegulatoryMappingBatchResponse, RegulatoryMappingBatchScope } from '../../domain/regulatory-mapping'; const assessorApi = new AssessorApiClient(); export interface RegulatoryMappingBatchController { response?: RegulatoryMappingBatchResponse; loading: boolean; starting: boolean; error?: string; start(): Promise; } export function useRegulatoryMappingBatch(projectId: string | undefined, scope: RegulatoryMappingBatchScope | undefined): RegulatoryMappingBatchController { const [response, setResponse] = useState(); const [loading, setLoading] = useState(false); const [starting, setStarting] = useState(false); const [error, setError] = useState(); const scopeKey = JSON.stringify(scope ?? null); const requestKey = `${projectId ?? ''}\u0000${scopeKey}`; const activeRequestKey = useRef(requestKey); activeRequestKey.current = requestKey; const refresh = useCallback(async () => { if (!projectId || !scope || (!scope.assessmentIds.length && !scope.scanRunIds.length)) { setResponse(undefined); return; } try { const next = await assessorApi.getRegulatoryMappingBatch(projectId, scope); if (activeRequestKey.current !== requestKey) return; setResponse(next); setError(undefined); } catch (reason) { if (activeRequestKey.current !== requestKey) return; setError(reason instanceof Error ? reason.message : 'ISO mapping batch status could not be loaded.'); } }, [projectId, requestKey, scopeKey]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { let active = true; setLoading(true); setStarting(false); setError(undefined); setResponse(undefined); void refresh().finally(() => { if (active) setLoading(false); }); return () => { active = false; }; }, [refresh, scopeKey]); useEffect(() => { if (!response || !['queued', 'running'].includes(response.state)) return; const timer = window.setTimeout(() => void refresh(), response.pollAfterMs ?? 1_500); return () => window.clearTimeout(timer); }, [refresh, response]); const start = useCallback(async () => { if (!projectId || !scope) return; setStarting(true); setError(undefined); try { const next = await assessorApi.startRegulatoryMappingBatch(projectId, { ...scope, idempotencyKey: `regulatory-batch-${crypto.randomUUID()}` }); if (activeRequestKey.current !== requestKey) return; setResponse(next); } catch (reason) { if (activeRequestKey.current !== requestKey) return; setError(reason instanceof Error ? reason.message : 'ISO mapping batch could not be started.'); } finally { if (activeRequestKey.current === requestKey) setStarting(false); } }, [projectId, requestKey, scopeKey]); // eslint-disable-line react-hooks/exhaustive-deps return { response, loading, starting, error, start }; } export function RegulatoryMappingBatchDialog({ controller, filteredResultCount, onClose }: { controller: RegulatoryMappingBatchController; filteredResultCount: number; onClose(): void; }) { const { response, loading, starting, error, start } = controller; const run = response?.run; const active = response && ['queued', 'running'].includes(response.state); const counts = run?.counts; const actionLabel = run ? 'Run again for current filters' : 'Start mapping batch'; return

Generate ISO control proposals for all {filteredResultCount.toLocaleString('en-US')} results matching the current Result DB filters.

Pagination, “Load more,” and loaded-row limits do not restrict this batch. Proposals remain subject to manual Accept/Reject review.

{loading && !response &&

Loading batch status…

} {error &&

{error}

} {response && !response.generationEnabled &&

{response.disabledReason ?? 'Regulatory mapping is unavailable.'}

} {run && <>

Status: {run.status === 'completed' && run.counts.failedRuleCount ? 'completed with failures' : run.status}

{run.errorDetail &&

{run.errorDetail}

} } {active &&

Mapping is running on the server. You can close this dialog and return later.

}
{response?.generationEnabled && }
; } function BatchCount({ label, value }: { label: string; value: number }) { return
{label}
{value.toLocaleString('en-US')}
; }