F4: replace director report review UI with INF workflow
This commit is contained in:
@@ -4,60 +4,97 @@ import { Link, useParams } from 'react-router';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||||
import { Icon } from '../components/Icon';
|
||||
import {
|
||||
approveInspectionReport,
|
||||
getInspectionReport,
|
||||
getInspectionReportReview,
|
||||
inspectionReportRevisionUrl,
|
||||
inspectionReportWordUrl,
|
||||
signFinalInspectionReport,
|
||||
uploadInspectionReportRevision,
|
||||
} from '../lib/api';
|
||||
import type { InspectionReport, InspectionReportReview } from '../lib/api';
|
||||
import { SearchableSelect } from '../components/SearchableSelect';
|
||||
import { formatDate } from '../lib/format';
|
||||
import {
|
||||
addInspectionReportFollowUp,
|
||||
getInspectionReportF4,
|
||||
inspectionReportWordDownloadUrl,
|
||||
listInspectionReportFollowUps,
|
||||
officializeInspectionReport,
|
||||
updateInspectionReportNarrative,
|
||||
} from '../lib/reportWorkflowApi';
|
||||
import type {
|
||||
InspectionReportDetailF4,
|
||||
InspectionReportFollowUp,
|
||||
InspectionReportFollowUpType,
|
||||
} from '../lib/reportWorkflowApi';
|
||||
|
||||
function names(items: Array<{ name: string }>, empty: string): string {
|
||||
return items.length ? items.map((item) => item.name).join(' · ') : empty;
|
||||
}
|
||||
|
||||
function reviewLabel(value: InspectionReport['reviewStatus']): string {
|
||||
if (value === 'SIGNED') return 'Firmado';
|
||||
if (value === 'APPROVED') return 'Aprobado · falta firma';
|
||||
return 'Pendiente de revisión';
|
||||
function localDateTime(value: Date): string {
|
||||
const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000);
|
||||
return local.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
function reviewClass(value: InspectionReport['reviewStatus']): string {
|
||||
if (value === 'SIGNED') return 'active';
|
||||
if (value === 'APPROVED') return 'observed';
|
||||
function reportStatusLabel(status: InspectionReportDetailF4['status']): string {
|
||||
if (status === 'WORKING') return 'En preparación';
|
||||
if (status === 'OFFICIALIZED') return 'Oficializado en GEDO';
|
||||
if (status === 'CANCELLED') return 'Cancelado';
|
||||
return 'Legado congelado';
|
||||
}
|
||||
|
||||
function reportStatusClass(status: InspectionReportDetailF4['status']): string {
|
||||
if (status === 'OFFICIALIZED') return 'active';
|
||||
if (status === 'CANCELLED') return 'danger';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
function revisionSource(value: 'AUTO' | 'DIRECTOR_UPLOAD'): string {
|
||||
return value === 'AUTO' ? 'Automática' : 'Corrección cargada';
|
||||
function followUpLabel(type: InspectionReportFollowUpType): string {
|
||||
if (type === 'COMPANY_NOTE') return 'Presentación / nota de empresa';
|
||||
if (type === 'COMPANY_DOCUMENT') return 'Documento de empresa';
|
||||
if (type === 'INTERNAL_NOTE') return 'Nota interna';
|
||||
if (type === 'VERIFICATION') return 'Verificación';
|
||||
return 'Otro antecedente';
|
||||
}
|
||||
|
||||
function fileSize(value: number | null): string {
|
||||
if (!value || value < 1) return '';
|
||||
if (value < 1024) return `${value} B`;
|
||||
if (value < 1024 * 1024) return `${Math.round(value / 1024)} KB`;
|
||||
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function ReportDetailPage() {
|
||||
const { id } = useParams();
|
||||
const { hasPermission } = useAuth();
|
||||
const [report, setReport] = useState<InspectionReport | null>(null);
|
||||
const [review, setReview] = useState<InspectionReportReview | null>(null);
|
||||
const canManage = hasPermission('inspection_reports.generate');
|
||||
const [report, setReport] = useState<InspectionReportDetailF4 | null>(null);
|
||||
const [followUps, setFollowUps] = useState<InspectionReportFollowUp[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [working, setWorking] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [revisionFile, setRevisionFile] = useState<File | null>(null);
|
||||
const [changeSummary, setChangeSummary] = useState('');
|
||||
const [approvalNote, setApprovalNote] = useState('');
|
||||
const [signatureConfirmed, setSignatureConfirmed] = useState(false);
|
||||
const canRevise = hasPermission('inspection_reports.revise');
|
||||
const canReview = hasPermission('inspection_reports.review');
|
||||
const canSign = hasPermission('inspection_reports.sign_final');
|
||||
|
||||
const [executiveSummary, setExecutiveSummary] = useState('');
|
||||
const [reportDescription, setReportDescription] = useState('');
|
||||
|
||||
const [gedoIfIdentifier, setGedoIfIdentifier] = useState('');
|
||||
const [gedoOfficializedAt, setGedoOfficializedAt] = useState(localDateTime(new Date()));
|
||||
const [gedoFile, setGedoFile] = useState<File | null>(null);
|
||||
|
||||
const [followUpType, setFollowUpType] = useState<InspectionReportFollowUpType>('COMPANY_NOTE');
|
||||
const [followUpOccurredAt, setFollowUpOccurredAt] = useState(localDateTime(new Date()));
|
||||
const [followUpReference, setFollowUpReference] = useState('');
|
||||
const [followUpDescription, setFollowUpDescription] = useState('');
|
||||
const [followUpFile, setFollowUpFile] = useState<File | null>(null);
|
||||
|
||||
const reload = async () => {
|
||||
if (!id) return;
|
||||
const [nextReport, nextReview] = await Promise.all([getInspectionReport(id), getInspectionReportReview(id)]);
|
||||
const [nextReport, nextFollowUps] = await Promise.all([
|
||||
getInspectionReportF4(id),
|
||||
listInspectionReportFollowUps(id),
|
||||
]);
|
||||
setReport(nextReport);
|
||||
setReview(nextReview);
|
||||
setFollowUps(nextFollowUps);
|
||||
setExecutiveSummary(nextReport.executiveSummary ?? '');
|
||||
setReportDescription(nextReport.reportDescription ?? '');
|
||||
setGedoIfIdentifier(nextReport.gedoIfIdentifier ?? '');
|
||||
if (nextReport.gedoOfficializedAt) {
|
||||
setGedoOfficializedAt(localDateTime(new Date(nextReport.gedoOfficializedAt)));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,19 +106,19 @@ export function ReportDetailPage() {
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
const uploadRevision = async (event: FormEvent) => {
|
||||
const saveNarrative = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || !revisionFile || changeSummary.trim().length < 5) return;
|
||||
if (!id || report?.status !== 'WORKING') return;
|
||||
setWorking(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
const next = await uploadInspectionReportRevision(id, revisionFile, changeSummary.trim());
|
||||
setReview(next);
|
||||
setRevisionFile(null);
|
||||
setChangeSummary('');
|
||||
setSuccess(`Versión ${next.currentRevisionNumber} agregada al historial.`);
|
||||
await updateInspectionReportNarrative(id, {
|
||||
executiveSummary: executiveSummary.trim() || null,
|
||||
description: reportDescription.trim() || null,
|
||||
});
|
||||
await reload();
|
||||
setSuccess('Contenido editable del INF actualizado. El Acta fuente y sus Hallazgos no fueron modificados.');
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
@@ -89,17 +126,21 @@ export function ReportDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const approve = async () => {
|
||||
if (!id) return;
|
||||
const officialize = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || !gedoFile || !gedoIfIdentifier.trim() || !gedoOfficializedAt) return;
|
||||
setWorking(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
const next = await approveInspectionReport(id, approvalNote);
|
||||
setReview(next);
|
||||
setApprovalNote('');
|
||||
setSuccess(`Versión ${next.currentRevisionNumber} aprobada por el Director.`);
|
||||
await officializeInspectionReport(id, {
|
||||
gedoIfIdentifier: gedoIfIdentifier.trim(),
|
||||
gedoOfficializedAt: new Date(gedoOfficializedAt).toISOString(),
|
||||
file: gedoFile,
|
||||
});
|
||||
setGedoFile(null);
|
||||
await reload();
|
||||
setSuccess('IF oficial de GEDO registrado. El PDF y su hash quedaron fijados de forma inmutable.');
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
@@ -107,17 +148,27 @@ export function ReportDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const signFinal = async () => {
|
||||
if (!id || !signatureConfirmed) return;
|
||||
const addFollowUp = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || !followUpOccurredAt) return;
|
||||
if (!followUpDescription.trim() && !followUpReference.trim() && !followUpFile) return;
|
||||
setWorking(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
try {
|
||||
const next = await signFinalInspectionReport(id);
|
||||
setReview(next);
|
||||
setSignatureConfirmed(false);
|
||||
setSuccess('Informe final firmado electrónicamente. La versión firmada quedó bloqueada.');
|
||||
await reload();
|
||||
const next = await addInspectionReportFollowUp(id, {
|
||||
type: followUpType,
|
||||
occurredAt: new Date(followUpOccurredAt).toISOString(),
|
||||
externalReference: followUpReference.trim() || null,
|
||||
description: followUpDescription.trim() || null,
|
||||
file: followUpFile,
|
||||
});
|
||||
setFollowUps(next);
|
||||
setFollowUpReference('');
|
||||
setFollowUpDescription('');
|
||||
setFollowUpFile(null);
|
||||
setFollowUpOccurredAt(localDateTime(new Date()));
|
||||
setSuccess('Antecedente agregado al seguimiento del INF. Los registros anteriores permanecen intactos.');
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
@@ -130,77 +181,99 @@ export function ReportDetailPage() {
|
||||
return <section>
|
||||
<div className="breadcrumb"><Link to="/informes">Informes</Link><span>/</span><span>{report?.code ?? 'Informe'}</span></div>
|
||||
<div className="page-heading survey-editor-heading">
|
||||
<div><span className="eyebrow">INFORME DE INSPECCIÓN</span><h1>{report?.title ?? 'Informe'}</h1><p>{report ? `${report.code} · emitido ${formatDate(report.generatedAt)}` : 'Consulta del informe.'}</p></div>
|
||||
{report && <div className="report-status-stack"><span className={`status-badge large ${report.wordStatus === 'READY' ? 'active' : report.wordStatus === 'FAILED' ? 'danger' : 'pending'}`}>{report.wordStatus === 'READY' ? 'Word disponible' : report.wordStatus === 'FAILED' ? 'Word con error' : 'Word pendiente'}</span><span className={`status-badge large ${reviewClass(report.reviewStatus)}`}>{reviewLabel(report.reviewStatus)}</span></div>}
|
||||
<div><span className="eyebrow">INFORME DE INSPECCIÓN</span><h1>{report?.code ?? 'Informe'}</h1><p>{report ? `Acta ${report.act.code} · generado ${formatDate(report.generatedAt)}` : 'Consulta del informe.'}</p></div>
|
||||
{report && <div className="report-status-stack">
|
||||
<span className={`status-badge large ${report.wordStatus === 'READY' ? 'active' : report.wordStatus === 'FAILED' ? 'danger' : 'pending'}`}>{report.wordStatus === 'READY' ? 'Word disponible' : report.wordStatus === 'FAILED' ? 'Word con error' : 'Word pendiente'}</span>
|
||||
<span className={`status-badge large ${reportStatusClass(report.status)}`}>{reportStatusLabel(report.status)}</span>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{success && <div className="success-banner">{success}</div>}
|
||||
{success && <Alert type="success">{success}</Alert>}
|
||||
|
||||
{report && <>
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Informe Word automático.</strong> La versión inicial se genera desde la instantánea congelada del Acta. Las correcciones se agregan como nuevas versiones y nunca sobrescriben la anterior.</p></div>
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Un INF corresponde a una sola Acta.</strong> El Word puede editarse durante la preparación del Informe. El Acta sellada y los Hallazgos que contiene permanecen inmutables.</p></div>
|
||||
|
||||
<section className="panel report-summary-panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">TRAZABILIDAD DOCUMENTAL</span><h2>{report.code}</h2></div><small className="muted">Versión del Acta: {report.actVersion}</small></div>
|
||||
<div className="responsible-summary">
|
||||
<div><small>Empresa</small><strong>{names(report.companies, 'Sin asignar')}</strong></div>
|
||||
<div><small>Área</small><strong>{names(report.areas, 'Sin asignar')}</strong></div>
|
||||
<div><small>Área / Yacimiento</small><strong>{names(report.areas, 'Sin asignar')}</strong></div>
|
||||
<div><small>Hallazgos</small><strong>{report.findingCount}</strong></div>
|
||||
<div><small>Emitido por</small><strong>{report.generatedBy.firstName} {report.generatedBy.lastName}</strong></div>
|
||||
<div><small>Generado por</small><strong>{report.generatedBy.firstName} {report.generatedBy.lastName}</strong></div>
|
||||
</div>
|
||||
{report.wordStatus === 'READY' && <div className="page-actions"><a className="button secondary" href={inspectionReportWordUrl(report.id)}>Descargar Word automático</a></div>}
|
||||
<div className="report-linked-documents">
|
||||
<Link to={`/inspecciones/${report.visitId}`}><span>Inspección</span><strong>{report.visit.code}</strong><Icon name="chevron" /></Link>
|
||||
<Link to={`/inspecciones/actas/${report.actId}`}><span>Acta incorporada</span><strong>{report.act.code}</strong><small>{report.act.title}</small><Icon name="chevron" /></Link>
|
||||
<Link to={`/hallazgos?search=${encodeURIComponent(report.act.code)}`}><span>Hallazgos</span><strong>{report.findingCount}</strong><small>Ver seguimiento relacionado</small><Icon name="chevron" /></Link>
|
||||
<Link to={`/inspecciones/actas/${report.actId}`}><span>Acta fuente</span><strong>{report.act.code}</strong><small>Contenido inmutable</small><Icon name="chevron" /></Link>
|
||||
<Link to={`/hallazgos?search=${encodeURIComponent(report.act.code)}`}><span>Hallazgos del Acta</span><strong>{report.findingCount}</strong><small>Seguimiento técnico</small><Icon name="chevron" /></Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel report-review-panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">REVISIÓN DIRECTIVA</span><h2>Versiones y firma final</h2><p className="section-copy">El Acta y los Hallazgos originales permanecen congelados. La revisión sólo versiona el documento Informe.</p></div>{review && <span className={`status-badge large ${reviewClass(review.reviewStatus)}`}>{reviewLabel(review.reviewStatus)}</span>}</div>
|
||||
{!review ? <LoadingBlock label="Cargando revisión…" /> : <>
|
||||
<div className="review-flow">
|
||||
<div className="complete"><span>1</span><strong>Informe automático</strong><small>{review.revisions.length ? 'Versión inicial registrada' : 'Esperando Word'}</small></div>
|
||||
<div className={review.currentRevisionNumber > 1 ? 'complete' : ''}><span>2</span><strong>Correcciones</strong><small>{review.currentRevisionNumber > 1 ? `${review.currentRevisionNumber - 1} versión/es agregada/s` : 'Sin correcciones'}</small></div>
|
||||
<div className={review.reviewStatus !== 'PENDING_REVIEW' ? 'complete' : ''}><span>3</span><strong>Aprobación</strong><small>{review.approvedAt ? formatDate(review.approvedAt) : 'Pendiente del Director'}</small></div>
|
||||
<div className={review.reviewStatus === 'SIGNED' ? 'complete' : ''}><span>4</span><strong>Firma final</strong><small>{review.signature ? formatDate(review.signature.signedAt) : 'Pendiente'}</small></div>
|
||||
<section className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">WORD EDITABLE</span><h2>Preparación del INF</h2><p className="section-copy">El Inspector puede revisar y ajustar el texto del Informe antes de incorporarlo a GEDO. Esta edición no altera el Acta fuente.</p></div>{report.wordStatus === 'READY' && <a className="button secondary" href={inspectionReportWordDownloadUrl(report.id)}>Descargar Word</a>}</div>
|
||||
<form className="form-section" onSubmit={saveNarrative}>
|
||||
<label className="field"><span>Resumen ejecutivo</span><textarea rows={4} maxLength={20000} value={executiveSummary} onChange={(event) => setExecutiveSummary(event.target.value)} disabled={!canManage || report.status !== 'WORKING'} placeholder="Resumen ejecutivo del Informe…" /></label>
|
||||
<label className="field"><span>Descripción / análisis técnico</span><textarea rows={8} maxLength={50000} value={reportDescription} onChange={(event) => setReportDescription(event.target.value)} disabled={!canManage || report.status !== 'WORKING'} placeholder="Descripción técnica, análisis y consideraciones del Inspector…" /></label>
|
||||
{canManage && report.status === 'WORKING' && <div className="form-actions"><button className="button primary" disabled={working}>{working ? 'Guardando…' : 'Guardar contenido del INF'}</button></div>}
|
||||
{report.status !== 'WORKING' && <Alert type="info">El contenido editable se cerró al registrar el IF oficial de GEDO.</Alert>}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">GEDO</span><h2>Oficialización del Informe</h2><p className="section-copy">Cuando GEDO devuelve el IF y el PDF oficial, ambos se registran en el sistema y pasan a ser la referencia documental institucional.</p></div></div>
|
||||
{report.status === 'OFFICIALIZED' ? <>
|
||||
<div className="responsible-summary">
|
||||
<div><small>Identificador IF</small><strong>{report.gedoIfIdentifier ?? '—'}</strong></div>
|
||||
<div><small>Oficializado</small><strong>{formatDate(report.gedoOfficializedAt)}</strong></div>
|
||||
<div><small>PDF oficial</small><strong>{report.gedoPdfOriginalName ?? 'Registrado'}</strong></div>
|
||||
<div><small>Vencimiento del Acta</small><strong>{formatDate(report.act.deadlineAt)}</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="report-revision-list">
|
||||
{review.revisions.map((revision) => <article key={revision.id} className={review.approvedRevisionId === revision.id ? 'approved' : ''}>
|
||||
<div><span className="status-badge">Versión {revision.revisionNumber}</span><strong>{revision.originalName}</strong><small>{revisionSource(revision.source)} · {revision.createdBy.firstName} {revision.createdBy.lastName} · {formatDate(revision.createdAt)}</small>{revision.changeSummary && <p>{revision.changeSummary}</p>}</div>
|
||||
<div className="revision-actions"><code title={revision.sha256}>{revision.sha256}</code><a className="button secondary" href={inspectionReportRevisionUrl(revision.id)}>Descargar</a></div>
|
||||
</article>)}
|
||||
{report.gedoPdfSha256 && <div className="temporal-notice"><Icon name="check" /><p><strong>PDF GEDO fijado.</strong> SHA-256: <code>{report.gedoPdfSha256}</code></p></div>}
|
||||
</> : canManage && report.status === 'WORKING' ? <form className="form-section" onSubmit={officialize}>
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Identificador IF de GEDO</span><input value={gedoIfIdentifier} onChange={(event) => setGedoIfIdentifier(event.target.value)} required maxLength={255} placeholder="IF-2026-…" /></label>
|
||||
<label className="field"><span>Fecha de oficialización</span><input type="datetime-local" value={gedoOfficializedAt} onChange={(event) => setGedoOfficializedAt(event.target.value)} required /></label>
|
||||
</div>
|
||||
<label className="field"><span>PDF oficial de GEDO</span><input type="file" accept="application/pdf,.pdf" onChange={(event) => setGedoFile(event.target.files?.[0] ?? null)} required /></label>
|
||||
<Alert>Esta acción cierra la edición del INF. El IF, la fecha y el hash del PDF oficial quedarán registrados como trazabilidad institucional.</Alert>
|
||||
<div className="form-actions"><button className="button primary" disabled={working || !gedoFile || !gedoIfIdentifier.trim()}>{working ? 'Registrando…' : 'Registrar IF y PDF oficial'}</button></div>
|
||||
</form> : <Alert type="info">El Informe todavía no está oficializado en GEDO.</Alert>}
|
||||
</section>
|
||||
|
||||
{canRevise && review.reviewStatus === 'PENDING_REVIEW' && <form className="report-review-form" onSubmit={uploadRevision}>
|
||||
<div><span className="eyebrow">NUEVA VERSIÓN</span><h3>Agregar Word corregido</h3><p className="section-copy">Descargá la última versión, realizá la corrección en Word y cargala nuevamente. La versión anterior queda intacta.</p></div>
|
||||
<label><span>Archivo .docx</span><input type="file" accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document" onChange={(event) => setRevisionFile(event.target.files?.[0] ?? null)} /></label>
|
||||
<label><span>Resumen de cambios</span><textarea value={changeSummary} onChange={(event) => setChangeSummary(event.target.value)} maxLength={1000} placeholder="Ej.: Se corrigió la conclusión técnica y la referencia del equipo inspeccionado." /></label>
|
||||
<button className="button primary" disabled={working || !revisionFile || changeSummary.trim().length < 5}>Agregar versión</button>
|
||||
</form>}
|
||||
<section className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">SEGUIMIENTO DEL INF</span><h2>Presentaciones y antecedentes</h2><p className="section-copy">Las respuestas de empresa, documentos, notas internas y verificaciones se agregan cronológicamente. Nunca reemplazan un antecedente anterior.</p></div><span className="count-pill">{followUps.length}</span></div>
|
||||
|
||||
{canReview && review.reviewStatus === 'PENDING_REVIEW' && review.revisions.length > 0 && <div className="report-review-action">
|
||||
<div><span className="eyebrow">APROBACIÓN</span><h3>Aprobar versión {review.currentRevisionNumber}</h3><p>La aprobación fija esta versión como candidata a firma final. Después de aprobar no se podrán cargar nuevas correcciones.</p></div>
|
||||
<label><span>Nota de revisión opcional</span><textarea value={approvalNote} onChange={(event) => setApprovalNote(event.target.value)} maxLength={1000} placeholder="Observación interna de aprobación" /></label>
|
||||
<button type="button" className="button primary" disabled={working} onClick={approve}>Aprobar versión actual</button>
|
||||
</div>}
|
||||
{followUps.length === 0 ? <div className="inline-empty">Todavía no hay antecedentes posteriores registrados.</div> : <div className="dossier-link-list">
|
||||
{[...followUps].reverse().map((item) => <div key={item.id}>
|
||||
<div>
|
||||
<strong>{followUpLabel(item.type)}</strong>
|
||||
<small>{item.description || item.externalReference || item.originalName || 'Sin descripción'}</small>
|
||||
{item.originalName && <small>Archivo: {item.originalName}{item.sizeBytes ? ` · ${fileSize(item.sizeBytes)}` : ''}</small>}
|
||||
</div>
|
||||
<span>{formatDate(item.occurredAt)}{item.externalReference ? ` · ${item.externalReference}` : ''}</span>
|
||||
</div>)}
|
||||
</div>}
|
||||
|
||||
{canSign && review.reviewStatus === 'APPROVED' && <div className="report-review-action final-signature-action">
|
||||
<div><span className="eyebrow">FIRMA FINAL</span><h3>Firma electrónica del Director</h3><p>La firma vincula de forma inmutable al Director, la versión aprobada, el hash del Informe y la fecha. Una vez firmada no admite nuevas versiones.</p></div>
|
||||
<label className="check-label"><input type="checkbox" checked={signatureConfirmed} onChange={(event) => setSignatureConfirmed(event.target.checked)} /><span>Confirmo que revisé la versión aprobada y firmo electrónicamente el informe final como Director de Hidrocarburos.</span></label>
|
||||
<button type="button" className="button primary" disabled={working || !signatureConfirmed} onClick={signFinal}>Firmar informe final</button>
|
||||
</div>}
|
||||
|
||||
{review.signature && <div className="signed-report-box"><Icon name="check" /><div><strong>Informe final firmado</strong><p>{review.signature.signedBy.firstName} {review.signature.signedBy.lastName} · {formatDate(review.signature.signedAt)}</p><code title={review.signature.signatureSha256}>{review.signature.signatureSha256}</code></div></div>}
|
||||
{review.reviewStatus === 'SIGNED' && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Firma cerrada.</strong> La firma electrónica ya prueba quién aprobó la versión y qué hash fue firmado. La ubicación visual de la rúbrica dentro del documento se resolverá con la plantilla institucional definitiva.</p></div>}
|
||||
</>}
|
||||
{canManage && <form className="form-section" onSubmit={addFollowUp}>
|
||||
<div><h3>Agregar antecedente</h3><p className="section-copy">Usá este bloque para registrar una nueva presentación sin modificar las anteriores.</p></div>
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Tipo</span><SearchableSelect value={followUpType} onChange={(event) => setFollowUpType(event.target.value as InspectionReportFollowUpType)}><option value="COMPANY_NOTE">Presentación / nota de empresa</option><option value="COMPANY_DOCUMENT">Documento de empresa</option><option value="INTERNAL_NOTE">Nota interna</option><option value="VERIFICATION">Verificación</option><option value="OTHER">Otro antecedente</option></SearchableSelect></label>
|
||||
<label className="field"><span>Fecha</span><input type="datetime-local" value={followUpOccurredAt} onChange={(event) => setFollowUpOccurredAt(event.target.value)} required /></label>
|
||||
<label className="field"><span>Referencia externa <em>opcional</em></span><input value={followUpReference} onChange={(event) => setFollowUpReference(event.target.value)} maxLength={255} placeholder="GEDO, expediente, nota, ticket…" /></label>
|
||||
</div>
|
||||
<label className="field"><span>Descripción</span><textarea rows={4} maxLength={20000} value={followUpDescription} onChange={(event) => setFollowUpDescription(event.target.value)} placeholder="Contenido o resumen de la presentación…" /></label>
|
||||
<label className="field"><span>Archivo <em>opcional</em></span><input type="file" onChange={(event) => setFollowUpFile(event.target.files?.[0] ?? null)} /></label>
|
||||
<div className="form-actions"><button className="button primary" disabled={working || (!followUpDescription.trim() && !followUpReference.trim() && !followUpFile)}>{working ? 'Agregando…' : 'Agregar al historial'}</button></div>
|
||||
</form>}
|
||||
</section>
|
||||
|
||||
<section className="panel report-integrity-panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">INTEGRIDAD</span><h2>Contenido congelado</h2><p className="section-copy">El Informe conserva la versión exacta del Acta cerrada y su instantánea documental.</p></div></div>
|
||||
<div className="panel-heading"><div><span className="eyebrow">INTEGRIDAD</span><h2>Fuente inmutable</h2><p className="section-copy">El INF conserva una copia verificable del Acta sellada que le dio origen.</p></div></div>
|
||||
<dl className="report-integrity-list">
|
||||
<div><dt>Acta fuente</dt><dd>{report.act.code}</dd></div>
|
||||
<div><dt>Hash de cierre del Acta</dt><dd>{report.actClosureSha256}</dd></div>
|
||||
<div><dt>Hash del Informe</dt><dd>{report.frozenSha256}</dd></div>
|
||||
<div><dt>Estado base</dt><dd>{report.status === 'FROZEN' ? 'Congelado' : 'Cancelado'}</dd></div>
|
||||
<div><dt>Revisión</dt><dd>{reviewLabel(report.reviewStatus)}</dd></div>
|
||||
<div><dt>Hash de la fuente del INF</dt><dd>{report.frozenSha256}</dd></div>
|
||||
<div><dt>Estado del INF</dt><dd>{reportStatusLabel(report.status)}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</>}
|
||||
|
||||
Reference in New Issue
Block a user