diff --git a/web-v2/src/features/inspections/FindingCatalogProposalsPanel.tsx b/web-v2/src/features/inspections/FindingCatalogProposalsPanel.tsx index b203dc5..b9e86d6 100644 --- a/web-v2/src/features/inspections/FindingCatalogProposalsPanel.tsx +++ b/web-v2/src/features/inspections/FindingCatalogProposalsPanel.tsx @@ -1,10 +1,11 @@ import { SearchableSelect } from '../../components/SearchableSelect'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router'; import { Alert, EmptyState, LoadingBlock, errorMessage } from '../../components/Feedback'; import { Icon } from '../../components/Icon'; import { listFindingCatalogProposals, reviewFindingCatalogProposal } from '../../lib/api'; import type { FindingAdminCatalog, FindingCatalogProposal } from '../../lib/api'; +import { mergeFindingCatalogItem } from '../../lib/findingCatalogMerge'; export function FindingCatalogProposalsPanel({ catalog }: { catalog: FindingAdminCatalog }) { const [items, setItems] = useState([]); @@ -14,6 +15,20 @@ export function FindingCatalogProposalsPanel({ catalog }: { catalog: FindingAdmi const [error, setError] = useState(''); const [success, setSuccess] = useState(''); + const [mergeSourceId, setMergeSourceId] = useState(''); + const [mergeCanonicalId, setMergeCanonicalId] = useState(''); + const [mergeReason, setMergeReason] = useState(''); + const [mergeBusy, setMergeBusy] = useState(false); + const [mergeError, setMergeError] = useState(''); + const [mergeSuccess, setMergeSuccess] = useState(''); + + const activeCatalogItems = useMemo( + () => catalog.items.filter((item) => item.isActive && item.categoryActive), + [catalog.items], + ); + const mergeSource = catalog.items.find((item) => item.id === mergeSourceId) ?? null; + const mergeCanonical = catalog.items.find((item) => item.id === mergeCanonicalId) ?? null; + const load = () => listFindingCatalogProposals({ status: 'PENDING' }).then(setItems); useEffect(() => { load().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false)); }, []); @@ -38,11 +53,86 @@ export function FindingCatalogProposalsPanel({ catalog }: { catalog: FindingAdmi } }; - if (loading) return
; + const mergeCatalog = async () => { + setMergeError(''); setMergeSuccess(''); + if (!mergeSourceId || !mergeCanonicalId) { + setMergeError('Seleccioná el registro duplicado y el registro canónico.'); + return; + } + if (mergeSourceId === mergeCanonicalId) { + setMergeError('El duplicado y el canónico deben ser registros distintos.'); + return; + } + if (mergeReason.trim().length < 8) { + setMergeError('Indicá un motivo de al menos 8 caracteres para que la conciliación quede auditada.'); + return; + } + const sourceLabel = mergeSource ? `${mergeSource.title} (${mergeSource.code})` : 'el duplicado'; + const canonicalLabel = mergeCanonical ? `${mergeCanonical.title} (${mergeCanonical.code})` : 'el canónico'; + if (!window.confirm(`¿Fusionar ${sourceLabel} dentro de ${canonicalLabel}?\n\nLos Hallazgos históricos conservarán su referencia original. El duplicado dejará de ofrecerse para inspecciones futuras.`)) return; - return
-
PROPUESTAS DESDE CAMPO

Hallazgos cargados como OTROS

El hallazgo original queda intacto. Esta bandeja sirve para decidir si conviene sumar esa opción al catálogo para inspecciones futuras.

{items.length} pendientes
- {error && {error}}{success && {success}} - {items.length === 0 ? :
{items.map((proposal) =>
{proposal.assetTypeName} · {proposal.assetCode}

{proposal.proposedTitle}

{proposal.description}

{proposal.findingCode} · {proposal.assetName}{proposal.proposedSeverity ? ` · gravedad ${proposal.proposedSeverity}/10` : ''}
Si todavía no existe una opción adecuada, creala en el catálogo y luego vinculá esta propuesta.
)}
} -
; + setMergeBusy(true); + try { + await mergeFindingCatalogItem(mergeSourceId, { + canonicalItemId: mergeCanonicalId, + reason: mergeReason.trim(), + }); + setMergeSuccess('Fusión registrada. El historial emitido quedó intacto y la aplicabilidad futura pasó al registro canónico.'); + setMergeSourceId(''); + setMergeCanonicalId(''); + setMergeReason(''); + window.setTimeout(() => window.location.reload(), 900); + } catch (requestError) { + setMergeError(errorMessage(requestError)); + } finally { + setMergeBusy(false); + } + }; + + return <> +
+
PROPUESTAS DESDE CAMPO

Hallazgos cargados como OTROS

El hallazgo original queda intacto. Esta bandeja sirve para decidir si conviene sumar esa opción al catálogo para inspecciones futuras.

{items.length} pendientes
+ {error && {error}}{success && {success}} + {loading ? : items.length === 0 ? :
{items.map((proposal) =>
{proposal.assetTypeName} · {proposal.assetCode}

{proposal.proposedTitle}

{proposal.description}

{proposal.findingCode} · {proposal.assetName}{proposal.proposedSeverity ? ` · gravedad ${proposal.proposedSeverity}/10` : ''}
Si todavía no existe una opción adecuada, creala en el catálogo y luego vinculá esta propuesta.
)}
} +
+ +
+
+
+ CONCILIACIÓN DE CATÁLOGO +

Fusionar Hallazgos duplicados

+

Usá esta operación cuando dos opciones del catálogo representan el mismo control. El duplicado se desactiva para el futuro, pero los Hallazgos y Actas ya emitidos conservan exactamente la referencia con la que nacieron.

+
+ append-only +
+ {mergeError && {mergeError}}{mergeSuccess && {mergeSuccess}} +
+ + +
+