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[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['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(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('EMAIL'); const [responseContactName, setResponseContactName] = useState(''); const [responseContactEmail, setResponseContactEmail] = useState(''); const [responseFile, setResponseFile] = useState(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
EXPEDIENTE OPERATIVO

Dar curso al hallazgo

Respuesta, documentación, vencimientos, verificación y cierre desde una sola ficha.

{workflow.label}
{error && {error}} {success && {success}}
1HallazgoRegistrado
2Empresa{hasResponse ? 'Respuesta recibida' : 'Esperando respuesta'}
3Verificación{verificationDone ? 'Resultado registrado' : finding.verificationVisit ? 'Visita planificada' : finding.nextControlOn ? 'Fecha definida' : 'Pendiente'}
4Cierre{closed ? 'Finalizado' : verificationReadyToClose ? 'Listo para cerrar' : 'Pendiente'}
{!closed &&
ACCIÓN RECOMENDADA{workflow.label}

{workflow.action}{workflow.dueOn ? ` Fecha: ${formatDateOnly(workflow.dueOn)}.` : ''}

{(workflow.stage === 'WAITING_COMPANY' || workflow.stage === 'COMPANY_OVERDUE') && Registrar respuesta} {(workflow.stage === 'DEFINE_VERIFICATION' || workflow.stage === 'RESCHEDULE_VERIFICATION') && Definir fecha} {workflow.stage === 'PLAN_VERIFICATION' && Planificar visita } {workflow.stage === 'VERIFICATION_PLANNED' && finding.verificationVisit && Abrir verificación } {workflow.stage === 'READY_TO_CLOSE' && Revisar y cerrar}
}
1 · ADMINISTRATIVO

Respuesta de la empresa

{hasResponse ? 'Recibida' : workflow.stage === 'COMPANY_OVERDUE' ? 'Vencida' : 'Pendiente'}
{!hasResponse && canFollowUp &&
{responseDueBasis === 'REPORT_NOTIFICATION' && }

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.

}
Vencimiento empresa{formatDateOnly(finding.correctionDueOn)}{finding.responseDueBasis && {finding.responseDueBasis === 'FINDING_DATE' ? 'Desde hallazgo' : 'Desde notificación'} · {finding.responseDueDays ?? 0} día{finding.responseDueDays === 1 ? '' : 's'} · base {formatDateOnly(finding.responseDueBaseOn)}}
{hasResponse &&
Respuesta recibida{formatDateOnly(finding.companyResponseReceivedOn)}
}
{!closed && canFollowUp &&