F4: remove obsolete finding company-response workspace

This commit is contained in:
2026-09-07 23:01:56 -03:00
parent 1775392eb1
commit 212f4dcee5
@@ -1,370 +0,0 @@
import { SearchableSelect } from '../../components/SearchableSelect';
import { useEffect, useRef, useState } from 'react';
import type { FormEvent } from 'react';
import { Link } from 'react-router';
import { Alert, errorMessage } from '../../components/Feedback';
import { Icon } from '../../components/Icon';
import {
closeInspectionFinding,
createInspectionFindingCommunication,
updateInspectionFindingFollowUp,
uploadInspectionFindingEvidence,
} from '../../lib/api';
import type {
InspectionFinding,
InspectionFindingCommunication,
InspectionFindingOfficeWorkflow,
} from '../../lib/api';
import { formatDate, formatDateOnly } from '../../lib/format';
function localDateTime(value: string | number | Date): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
return local.toISOString().slice(0, 16);
}
function workflowFallback(finding: InspectionFinding): InspectionFindingOfficeWorkflow {
if (finding.status === 'CLOSED') {
return { stage: 'CLOSED', label: 'Hallazgo cerrado', action: 'El seguimiento está finalizado.', dueOn: null, overdue: false };
}
if (!finding.companyResponseReceivedOn) {
return { stage: 'WAITING_COMPANY', label: 'Esperando respuesta de empresa', action: 'Registrar la respuesta cuando sea recibida.', dueOn: finding.correctionDueOn, overdue: false };
}
if (finding.latestVerification?.outcome === 'RESOLVED' && finding.latestVerification.visitStatus === 'CLOSED') {
return { stage: 'READY_TO_CLOSE', label: 'Listo para cierre', action: 'Revisar la evidencia y cerrar administrativamente.', dueOn: null, overdue: false };
}
if (!finding.nextControlOn) {
return { stage: 'DEFINE_VERIFICATION', label: 'Definir fecha de verificación', action: 'Establecer cuándo debe volver a controlarse.', dueOn: null, overdue: false };
}
if (finding.verificationVisit) {
return { stage: 'VERIFICATION_PLANNED', label: 'Verificación planificada', action: 'Consultar la visita vinculada.', dueOn: finding.nextControlOn, overdue: false };
}
return { stage: 'PLAN_VERIFICATION', label: 'Verificación pendiente de planificación', action: 'Crear la visita de verificación.', dueOn: finding.nextControlOn, overdue: false };
}
function verificationEventLabel(eventType: NonNullable<InspectionFinding['verificationHistory']>[number]['eventType']): string {
if (eventType === 'CONTROL_DATE_DEFINED') return 'Fecha de verificación definida';
if (eventType === 'CONTROL_DATE_CHANGED') return 'Fecha de verificación reprogramada';
if (eventType === 'CONTROL_DATE_CLEARED') return 'Fecha de verificación consumida o retirada';
if (eventType === 'VISIT_PLANNED') return 'Visita de verificación planificada';
return 'Resultado de verificación registrado';
}
function verificationOutcomeLabel(outcome: NonNullable<InspectionFinding['latestVerification']>['outcome']): string {
if (outcome === 'RESOLVED') return 'Solucionado';
if (outcome === 'NOT_RESOLVED') return 'No solucionado';
if (outcome === 'REQUIRES_NEW_DATE') return 'Requiere nueva fecha';
return '';
}
export function FindingOfficeWorkspace({
finding,
canFollowUp,
canClose,
canCreateEvidence,
canCreateCommunications,
onFindingChanged,
onEvidenceChanged,
}: {
finding: InspectionFinding;
canFollowUp: boolean;
canClose: boolean;
canCreateEvidence: boolean;
canCreateCommunications: boolean;
onFindingChanged: (finding: InspectionFinding) => void;
onEvidenceChanged: () => void;
}) {
const responseFileRef = useRef<HTMLInputElement | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [responseText, setResponseText] = useState('');
const [responseOccurredAt, setResponseOccurredAt] = useState(localDateTime(new Date()));
const [responseChannel, setResponseChannel] = useState<InspectionFindingCommunication['channel']>('EMAIL');
const [responseContactName, setResponseContactName] = useState('');
const [responseContactEmail, setResponseContactEmail] = useState('');
const [responseFile, setResponseFile] = useState<File | null>(null);
const [responseDueBasis, setResponseDueBasis] = useState<'FINDING_DATE' | 'REPORT_NOTIFICATION' | ''>(finding.responseDueBasis ?? '');
const [responseDueDays, setResponseDueDays] = useState(finding.responseDueDays == null ? '' : String(finding.responseDueDays));
const [reportNotifiedOn, setReportNotifiedOn] = useState(finding.reportNotifiedOn ?? '');
const [committedOn, setCommittedOn] = useState(finding.companyCommittedCorrectionOn ?? '');
const [nextControlOn, setNextControlOn] = useState(finding.nextControlOn ?? '');
const [closureNotes, setClosureNotes] = useState('');
useEffect(() => {
setResponseText('');
setResponseDueBasis(finding.responseDueBasis ?? '');
setResponseDueDays(finding.responseDueDays == null ? '' : String(finding.responseDueDays));
setReportNotifiedOn(finding.reportNotifiedOn ?? '');
setCommittedOn(finding.companyCommittedCorrectionOn ?? '');
setNextControlOn(finding.nextControlOn ?? '');
}, [finding.id, finding.responseDueBasis, finding.responseDueDays, finding.reportNotifiedOn, finding.companyCommittedCorrectionOn, finding.nextControlOn]);
const workflow = finding.officeWorkflow ?? workflowFallback(finding);
const hasResponse = Boolean(finding.companyResponseReceivedOn);
const verificationDone = Boolean(finding.latestVerification?.outcome);
const closed = finding.status === 'CLOSED';
const verificationReadyToClose = finding.latestVerification?.outcome === 'RESOLVED'
&& finding.latestVerification.visitStatus === 'CLOSED';
const verificationActive = Boolean(finding.verificationVisit);
const verificationHistory = finding.verificationHistory ?? [];
const saveResponseDeadline = async (event: FormEvent) => {
event.preventDefault();
if (!responseDueBasis || responseDueDays === '') return;
if (responseDueBasis === 'REPORT_NOTIFICATION' && !reportNotifiedOn) return;
setBusy(true);
setError('');
setSuccess('');
try {
const updated = await updateInspectionFindingFollowUp(finding.id, {
responseDueBasis,
responseDueDays: Number(responseDueDays),
reportNotifiedOn: responseDueBasis === 'REPORT_NOTIFICATION' ? reportNotifiedOn : null,
});
onFindingChanged(updated);
setSuccess('Plazo administrativo calculado y guardado con su fecha base.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const registerResponse = async (event: FormEvent) => {
event.preventDefault();
if (!responseText.trim() || !responseOccurredAt) return;
const receivedOn = responseOccurredAt.slice(0, 10);
const firstPresentation = !hasResponse;
setBusy(true);
setError('');
setSuccess('');
try {
let updated = finding;
if (firstPresentation) {
updated = await updateInspectionFindingFollowUp(finding.id, {
companyResponse: responseText.trim(),
companyResponseReceivedOn: receivedOn,
companyCommittedCorrectionOn: committedOn || null,
nextControlOn: nextControlOn || null,
});
onFindingChanged(updated);
} else if (
committedOn !== (finding.companyCommittedCorrectionOn ?? '')
|| nextControlOn !== (finding.nextControlOn ?? '')
) {
updated = await updateInspectionFindingFollowUp(finding.id, {
companyCommittedCorrectionOn: committedOn || null,
nextControlOn: nextControlOn || null,
});
onFindingChanged(updated);
}
let communicationId = '';
let traceWarning = '';
if (canCreateCommunications) {
try {
const communication = await createInspectionFindingCommunication(finding.id, {
type: 'COMPANY_RESPONSE',
direction: 'INBOUND',
channel: responseChannel,
occurredAt: new Date(responseOccurredAt).toISOString(),
subject: `${firstPresentation ? 'Respuesta' : 'Nueva presentación'} de empresa · ${finding.code}`,
details: responseText.trim(),
contactName: responseContactName.trim() || null,
contactEmail: responseContactEmail.trim() || null,
});
communicationId = communication.id;
onEvidenceChanged();
} catch (requestError) {
traceWarning = `${firstPresentation ? 'La primera respuesta' : 'La actualización de seguimiento'} quedó registrada, pero la presentación histórica no pudo incorporarse: ${errorMessage(requestError)}`;
}
} else {
traceWarning = 'No tenés permiso para incorporar la presentación a la trazabilidad histórica.';
}
if (responseFile && canCreateEvidence) {
if (!communicationId) {
traceWarning = traceWarning || 'El documento debe incorporarse desde Documentos y trazabilidad porque no existe una presentación histórica vinculada.';
} else {
try {
await uploadInspectionFindingEvidence(finding.id, {
file: responseFile,
kind: 'DOCUMENT',
purpose: 'COMPANY_RESPONSE',
communicationId,
title: responseFile.name,
description: `Documento presentado por la empresa para ${finding.code}`,
capturedAt: new Date(responseOccurredAt).toISOString(),
deviceLabel: navigator.userAgent.slice(0, 200),
});
onEvidenceChanged();
} catch (requestError) {
traceWarning = `La presentación quedó registrada, pero el documento no pudo incorporarse: ${errorMessage(requestError)}`;
}
}
}
setResponseText('');
setResponseContactName('');
setResponseContactEmail('');
setResponseOccurredAt(localDateTime(new Date()));
setResponseFile(null);
if (responseFileRef.current) responseFileRef.current.value = '';
setSuccess(firstPresentation
? 'Primera respuesta registrada. Las presentaciones posteriores se agregarán sin modificar esta respuesta original.'
: 'Nueva presentación agregada al historial del hallazgo. La respuesta original permanece inmutable.');
if (traceWarning) setError(traceWarning);
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const saveOperationalFollowUp = async (event: FormEvent) => {
event.preventDefault();
setBusy(true);
setError('');
setSuccess('');
try {
const updated = await updateInspectionFindingFollowUp(finding.id, {
companyCommittedCorrectionOn: committedOn || null,
nextControlOn: nextControlOn || null,
});
onFindingChanged(updated);
setSuccess('Seguimiento operativo actualizado. El cambio de fecha quedó agregado al historial sin borrar los controles anteriores.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const closeFinding = async (event: FormEvent) => {
event.preventDefault();
if (!closureNotes.trim()) return;
setBusy(true);
setError('');
setSuccess('');
try {
const updated = await closeInspectionFinding(finding.id, closureNotes.trim());
onFindingChanged(updated);
setClosureNotes('');
setSuccess('Hallazgo cerrado. La conclusión quedó incorporada al expediente histórico.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
return <article className="panel finding-office-workspace">
<div className="panel-heading">
<div><span className="eyebrow">EXPEDIENTE OPERATIVO</span><h2>Dar curso al hallazgo</h2><p className="section-copy">Respuesta, documentación, vencimientos, verificación y cierre desde una sola ficha.</p></div>
<span className={`status-badge large ${workflow.overdue ? 'observed' : workflow.stage === 'CLOSED' ? 'active' : 'pending'}`}>{workflow.label}</span>
</div>
{error && <Alert>{error}</Alert>}
{success && <Alert type="success">{success}</Alert>}
<div className="finding-case-progress" aria-label="Etapas del seguimiento">
<div className="done"><span>1</span><strong>Hallazgo</strong><small>Registrado</small></div>
<i></i>
<div className={hasResponse ? 'done' : 'current'}><span>2</span><strong>Empresa</strong><small>{hasResponse ? 'Respuesta recibida' : 'Esperando respuesta'}</small></div>
<i></i>
<div className={verificationDone ? 'done' : hasResponse ? 'current' : 'pending'}><span>3</span><strong>Verificación</strong><small>{verificationDone ? 'Resultado registrado' : finding.verificationVisit ? 'Visita planificada' : finding.nextControlOn ? 'Fecha definida' : 'Pendiente'}</small></div>
<i></i>
<div className={closed ? 'done' : verificationReadyToClose ? 'current' : 'pending'}><span>4</span><strong>Cierre</strong><small>{closed ? 'Finalizado' : verificationReadyToClose ? 'Listo para cerrar' : 'Pendiente'}</small></div>
</div>
{!closed && <div className={`finding-recommended-action ${workflow.overdue ? 'overdue' : ''}`}>
<div><span className="eyebrow">ACCIÓN RECOMENDADA</span><strong>{workflow.label}</strong><p>{workflow.action}{workflow.dueOn ? ` Fecha: ${formatDateOnly(workflow.dueOn)}.` : ''}</p></div>
<div className="form-actions">
{(workflow.stage === 'WAITING_COMPANY' || workflow.stage === 'COMPANY_OVERDUE') && <a className="button primary" href="#respuesta-empresa">Registrar respuesta</a>}
{(workflow.stage === 'DEFINE_VERIFICATION' || workflow.stage === 'RESCHEDULE_VERIFICATION') && <a className="button primary" href="#seguimiento-operativo">Definir fecha</a>}
{workflow.stage === 'PLAN_VERIFICATION' && <Link className="button primary" to="/hallazgos/planificacion">Planificar visita <Icon name="chevron" /></Link>}
{workflow.stage === 'VERIFICATION_PLANNED' && finding.verificationVisit && <Link className="button primary" to={`/inspecciones/${finding.verificationVisit.id}`}>Abrir verificación <Icon name="chevron" /></Link>}
{workflow.stage === 'READY_TO_CLOSE' && <a className="button primary" href="#cierre-hallazgo">Revisar y cerrar</a>}
</div>
</div>}
<div className="finding-office-sections">
<section id="respuesta-empresa" className={`finding-office-step ${hasResponse ? 'complete' : ''}`}>
<div className="subsection-heading"><div><span className="eyebrow">1 · ADMINISTRATIVO</span><h3>Respuesta de la empresa</h3></div><span className={`status-badge ${hasResponse ? 'active' : workflow.stage === 'COMPANY_OVERDUE' ? 'observed' : 'pending'}`}>{hasResponse ? 'Recibida' : workflow.stage === 'COMPANY_OVERDUE' ? 'Vencida' : 'Pendiente'}</span></div>
{!hasResponse && canFollowUp && <form className="finding-office-form finding-deadline-form" onSubmit={saveResponseDeadline}>
<div className="form-grid">
<label className="field"><span>Inicio del plazo de respuesta</span><SearchableSelect value={responseDueBasis} onChange={(event) => setResponseDueBasis(event.target.value as 'FINDING_DATE' | 'REPORT_NOTIFICATION' | '')}><option value="">Seleccionar</option><option value="FINDING_DATE">Desde el día del hallazgo</option><option value="REPORT_NOTIFICATION">Desde la notificación del informe</option></SearchableSelect></label>
<label className="field"><span>Cantidad de días</span><input type="number" min="0" max="3650" value={responseDueDays} onChange={(event) => setResponseDueDays(event.target.value)} placeholder="Ej.: 5" /></label>
{responseDueBasis === 'REPORT_NOTIFICATION' && <label className="field"><span>Informe notificado el</span><input type="date" value={reportNotifiedOn} onChange={(event) => setReportNotifiedOn(event.target.value)} /></label>}
</div>
<p className="field-help">DH guarda la regla, la fecha base y el vencimiento calculado. Para casos críticos puede computarse desde el hallazgo; para los demás, desde la notificación del informe.</p>
<div className="form-actions"><button className="button secondary" disabled={busy || !responseDueBasis || responseDueDays === '' || (responseDueBasis === 'REPORT_NOTIFICATION' && !reportNotifiedOn)}><Icon name="calendar" />Calcular vencimiento</button></div>
</form>}
<div className="finding-office-deadline-row"><div><small>Vencimiento empresa</small><strong>{formatDateOnly(finding.correctionDueOn)}</strong>{finding.responseDueBasis && <span>{finding.responseDueBasis === 'FINDING_DATE' ? 'Desde hallazgo' : 'Desde notificación'} · {finding.responseDueDays ?? 0} día{finding.responseDueDays === 1 ? '' : 's'} · base {formatDateOnly(finding.responseDueBaseOn)}</span>}</div>{hasResponse && <div><small>Respuesta recibida</small><strong>{formatDateOnly(finding.companyResponseReceivedOn)}</strong></div>}</div>
{!closed && canFollowUp && <form className="finding-office-form" onSubmit={registerResponse}>
<label className="field"><span>{hasResponse ? 'Nueva presentación / ampliación de la empresa' : 'Respuesta o descargo recibido'}</span><textarea required rows={5} maxLength={20000} value={responseText} onChange={(event) => setResponseText(event.target.value)} placeholder={hasResponse ? 'Registrar sólo la nueva presentación; la respuesta original no se modifica…' : 'Resumir la presentación, acción correctiva o descargo recibido…'} /></label>
<div className="form-grid finding-response-grid">
<label className="field"><span>Recibida el</span><input type="datetime-local" required value={responseOccurredAt} onChange={(event) => setResponseOccurredAt(event.target.value)} /></label>
<label className="field"><span>Medio</span><SearchableSelect value={responseChannel} onChange={(event) => setResponseChannel(event.target.value as InspectionFindingCommunication['channel'])}><option value="EMAIL">Email</option><option value="LETTER">Nota formal</option><option value="IN_PERSON">Presencial</option><option value="PHONE">Teléfono</option><option value="SYSTEM">Sistema</option><option value="OTHER">Otro medio</option></SearchableSelect></label>
<label className="field"><span>Fecha informada por la empresa <em>opcional</em></span><input type="date" value={committedOn} onChange={(event) => setCommittedOn(event.target.value)} /></label>
<label className="field"><span>Fecha de verificación <em>opcional</em></span><input type="date" value={nextControlOn} onChange={(event) => setNextControlOn(event.target.value)} /></label>
</div>
{canCreateCommunications && <div className="form-grid"><label className="field"><span>Contacto <em>opcional</em></span><input maxLength={200} value={responseContactName} onChange={(event) => setResponseContactName(event.target.value)} /></label><label className="field"><span>Email de contacto <em>opcional</em></span><input type="email" maxLength={320} value={responseContactEmail} onChange={(event) => setResponseContactEmail(event.target.value)} /></label></div>}
{canCreateCommunications && canCreateEvidence && <label className="field"><span>Documento presentado por la empresa <em>opcional</em></span><input ref={responseFileRef} type="file" accept="application/pdf" onChange={(event) => setResponseFile(event.target.files?.[0] ?? null)} /><small className="field-help">Si adjuntás un PDF, queda vinculado a la respuesta recibida y protegido con SHA-256.</small></label>}
<p className="field-help">Registrar la respuesta cumple el vencimiento administrativo. La fecha informada por la empresa es sólo un compromiso declarado y la fecha de verificación sigue siendo una decisión operativa de DH.</p>
<div className="form-actions"><button className="button primary" disabled={busy || !responseText.trim() || !responseOccurredAt}><Icon name="check" />{busy ? 'Registrando…' : hasResponse ? 'Agregar presentación' : 'Registrar respuesta'}</button></div>
</form>}
{hasResponse && <div className="finding-response-record">
<div><small>Primera respuesta registrada · inmutable</small><p>{finding.companyResponse}</p></div>
<div><small>Fecha informada por la empresa</small><strong>{formatDateOnly(finding.companyCommittedCorrectionOn)}</strong></div>
</div>}
</section>
<section id="seguimiento-operativo" className={`finding-office-step ${finding.nextControlOn || verificationDone ? 'complete' : ''}`}>
<div className="subsection-heading"><div><span className="eyebrow">2 · OPERATIVO</span><h3>Verificación del hallazgo</h3></div><span className={`status-badge ${verificationDone ? 'active' : finding.nextControlOn ? 'pending' : 'draft'}`}>{verificationDone ? 'Verificado' : finding.nextControlOn ? 'Fecha definida' : 'Sin fecha'}</span></div>
{hasResponse && canFollowUp && !closed && <form className="finding-office-form" onSubmit={saveOperationalFollowUp}>
<div className="form-grid finding-follow-up-dates">
<label className="field"><span>Fecha informada por la empresa</span><input type="date" value={committedOn} onChange={(event) => setCommittedOn(event.target.value)} /></label>
<label className="field"><span>Fecha de verificación / resolución</span><input type="date" value={nextControlOn} onChange={(event) => setNextControlOn(event.target.value)} /></label>
</div>
<p className="field-help">El vencimiento administrativo original permanece intacto. Esta segunda fecha alimenta la planificación de verificaciones e inspecciones.</p>
<div className="form-actions"><button className="button secondary" disabled={busy}><Icon name="check" />Guardar seguimiento</button>{nextControlOn && !finding.verificationVisit && finding.latestVerification?.outcome !== 'RESOLVED' && <Link className="button primary" to="/hallazgos/planificacion">Planificar verificación <Icon name="chevron" /></Link>}{finding.verificationVisit && <Link className="button secondary" to={`/inspecciones/${finding.verificationVisit.id}`}>Abrir visita <Icon name="chevron" /></Link>}</div>
</form>}
{!hasResponse && <div className="inline-empty">La planificación operativa se habilita cuando se registra la respuesta de la empresa.</div>}
{finding.latestVerification?.outcome && <div className="finding-verification-record"><small>Último resultado de campo</small><strong>{verificationOutcomeLabel(finding.latestVerification.outcome)}</strong><span>{formatDate(finding.latestVerification.verifiedAt)} · {finding.latestVerification.visitCode}</span><p>{finding.latestVerification.resultNotes || 'Sin observaciones adicionales.'}</p></div>}
{verificationHistory.length > 0 && <div className="finding-verification-history">
<div className="finding-verification-history-heading"><div><small>TRAZABILIDAD APPEND-ONLY</small><strong>Historial completo de verificaciones</strong></div><span>{verificationHistory.length} evento{verificationHistory.length === 1 ? '' : 's'}</span></div>
<div className="finding-verification-timeline">{verificationHistory.map((event) => <div className="finding-verification-event" key={event.id}>
<div className="finding-verification-event-dot" />
<div className="finding-verification-event-body">
<div className="finding-verification-event-title"><strong>{verificationEventLabel(event.eventType)}</strong><span>{formatDate(event.occurredAt)}</span></div>
{event.outcome && <span className="status-badge pending">{verificationOutcomeLabel(event.outcome)}</span>}
{event.eventType.startsWith('CONTROL_DATE_') && (event.previousControlOn || event.nextControlOn) && <div className="finding-verification-date-change"><small>Fecha anterior</small><strong>{formatDateOnly(event.previousControlOn)}</strong><i></i><small>Nueva fecha</small><strong>{formatDateOnly(event.nextControlOn)}</strong></div>}
{(event.eventType === 'VISIT_PLANNED' || event.eventType === 'RESULT_RECORDED') && event.targetControlOn && <div className="finding-verification-target-date"><small>{event.eventType === 'VISIT_PLANNED' ? 'Control previsto' : 'Fecha que se verificó'}</small><strong>{formatDateOnly(event.targetControlOn)}</strong></div>}
{event.verificationVisit && <Link className="finding-verification-visit-link" to={`/inspecciones/${event.verificationVisit.id}`}><span>{event.verificationVisit.code}</span><Icon name="chevron" /></Link>}
{event.notes && <p>{event.notes}</p>}
<div className="finding-verification-event-meta"><span>{event.actorUsername || 'Sistema'}</span>{event.evidenceCount > 0 && <span>{event.evidenceCount} evidencia{event.evidenceCount === 1 ? '' : 's'}</span>}</div>
</div>
</div>)}</div>
</div>}
</section>
<section id="cierre-hallazgo" className={`finding-office-step ${closed ? 'complete' : ''}`}>
<div className="subsection-heading"><div><span className="eyebrow">3 · CIERRE</span><h3>{closed ? 'Hallazgo cerrado' : verificationReadyToClose ? 'Listo para cierre' : 'Decisión administrativa'}</h3></div><span className={`status-badge ${closed ? 'active' : verificationReadyToClose ? 'active' : 'draft'}`}>{closed ? 'Cerrado' : verificationReadyToClose ? 'Revisar' : 'Pendiente'}</span></div>
{closed ? <div className="finding-response-record"><div><small>Cerrado el</small><strong>{formatDate(finding.closedAt)}</strong><p>{finding.closureNotes}</p></div></div> : canClose ? <form className="finding-office-form" onSubmit={closeFinding}>
{verificationReadyToClose && <Alert type="success">La verificación de campo indicó que el hallazgo está solucionado. Revisá fotos y documentos antes de registrar el cierre.</Alert>}
<label className="field"><span>Conclusión de cierre</span><textarea rows={4} maxLength={5000} value={closureNotes} onChange={(event) => setClosureNotes(event.target.value)} placeholder="Indicar qué se revisó, qué respaldo existe y por qué corresponde cerrar el hallazgo…" /></label>
<p className="field-help">{verificationActive ? 'Existe una visita de verificación activa. Debe finalizar antes del cierre administrativo.' : 'El cierre queda versionado y auditado. Los documentos y fotos continúan disponibles en el expediente.'}</p>
<div className="form-actions"><a className="button text" href="#documentos-hallazgo">Revisar documentos</a><button className="button danger-outline" disabled={busy || verificationActive || closureNotes.trim().length < 3}><Icon name="check" />Cerrar hallazgo</button></div>
</form> : <div className="inline-empty">El cierre está reservado a Inspector, Supervisor o Director.</div>}
</section>
</div>
{hasResponse && <div className="finding-office-footnote"><Icon name="history" /><span>Respuesta recibida {formatDateOnly(finding.companyResponseReceivedOn)} · Las presentaciones sucesivas, medios y documentos quedan en la trazabilidad sin modificar la respuesta original. {finding.companyCommittedCorrectionOn ? `La empresa informó ${formatDateOnly(finding.companyCommittedCorrectionOn)}.` : 'La empresa no informó una fecha de corrección.'}</span></div>}
</article>;
}