49 lines
4.2 KiB
TypeScript
49 lines
4.2 KiB
TypeScript
import { SearchableSelect } from '../../components/SearchableSelect';
|
|
import { useEffect, 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';
|
|
|
|
export function FindingCatalogProposalsPanel({ catalog }: { catalog: FindingAdminCatalog }) {
|
|
const [items, setItems] = useState<FindingCatalogProposal[]>([]);
|
|
const [matches, setMatches] = useState<Record<string, string>>({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [busyId, setBusyId] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState('');
|
|
|
|
const load = () => listFindingCatalogProposals({ status: 'PENDING' }).then(setItems);
|
|
useEffect(() => { load().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false)); }, []);
|
|
|
|
const review = async (proposal: FindingCatalogProposal, decision: 'MATCH' | 'REJECT') => {
|
|
setBusyId(proposal.id); setError(''); setSuccess('');
|
|
try {
|
|
if (decision === 'MATCH' && !matches[proposal.id]) {
|
|
setError('Seleccioná primero el tipo de hallazgo del catálogo que corresponde.');
|
|
return;
|
|
}
|
|
await reviewFindingCatalogProposal(proposal.id, {
|
|
decision,
|
|
catalogItemId: decision === 'MATCH' ? matches[proposal.id] : undefined,
|
|
notes: decision === 'REJECT' ? 'Propuesta descartada desde administración de catálogo.' : 'Propuesta vinculada a un tipo existente del catálogo.',
|
|
});
|
|
await load();
|
|
setSuccess(decision === 'MATCH' ? 'Propuesta incorporada al criterio de catálogo futuro.' : 'Propuesta descartada sin modificar el hallazgo histórico.');
|
|
} catch (requestError) {
|
|
setError(errorMessage(requestError));
|
|
} finally {
|
|
setBusyId('');
|
|
}
|
|
};
|
|
|
|
if (loading) return <div className="panel"><LoadingBlock label="Cargando propuestas OTROS…" /></div>;
|
|
|
|
return <section className="panel finding-proposals-panel">
|
|
<div className="panel-heading"><div><span className="eyebrow">PROPUESTAS DESDE CAMPO</span><h2>Hallazgos cargados como OTROS</h2><p className="section-copy">El hallazgo original queda intacto. Esta bandeja sirve para decidir si conviene sumar esa opción al catálogo para inspecciones futuras.</p></div><span className="count-pill">{items.length} pendientes</span></div>
|
|
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
|
|
{items.length === 0 ? <EmptyState title="Sin propuestas pendientes" text="Los nuevos hallazgos cargados como OTROS aparecerán acá." /> : <div className="finding-proposal-list">{items.map((proposal) => <article className="finding-proposal-card" key={proposal.id}><div><span className="eyebrow">{proposal.assetTypeName} · {proposal.assetCode}</span><h3>{proposal.proposedTitle}</h3><p>{proposal.description}</p><small><Link to={`/hallazgos/${proposal.findingId}`}>{proposal.findingCode}</Link> · {proposal.assetName}{proposal.proposedSeverity ? ` · gravedad ${proposal.proposedSeverity}/10` : ''}</small></div><div className="finding-proposal-actions"><label className="field"><span>Coincide con</span><SearchableSelect value={matches[proposal.id] ?? ''} onChange={(event) => setMatches((current) => ({ ...current, [proposal.id]: event.target.value }))}><option value="">Seleccionar tipo existente…</option>{catalog.items.filter((item) => item.isActive && item.categoryActive).map((item) => <option value={item.id} key={item.id}>{item.title} · {item.code}</option>)}</SearchableSelect></label><div className="form-actions"><button type="button" className="button secondary" disabled={busyId === proposal.id} onClick={() => review(proposal, 'REJECT')}>Descartar</button><button type="button" className="button primary" disabled={busyId === proposal.id || !matches[proposal.id]} onClick={() => review(proposal, 'MATCH')}><Icon name="check" />Vincular</button></div><small>Si todavía no existe una opción adecuada, creala en el catálogo y luego vinculá esta propuesta.</small></div></article>)}</div>}
|
|
</section>;
|
|
}
|