feat(informes): manage manual GEDO responses and shared due date
DH V2 CI / WEB · typecheck, build (push) Successful in 27s
Production dependency audit / API · production dependencies (push) Successful in 15s
Production dependency audit / WEB · production dependencies (push) Successful in 15s
DH V2 CI / API · typecheck, tests, build (push) Successful in 2m3s
DH V2 CI / Docker / migrations / production images (push) Successful in 2m58s
DH V2 CI / Promote verified main to deploy (push) Successful in 3s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 5m55s
DH V2 CI / WEB · typecheck, build (push) Successful in 27s
Production dependency audit / API · production dependencies (push) Successful in 15s
Production dependency audit / WEB · production dependencies (push) Successful in 15s
DH V2 CI / API · typecheck, tests, build (push) Successful in 2m3s
DH V2 CI / Docker / migrations / production images (push) Successful in 2m58s
DH V2 CI / Promote verified main to deploy (push) Successful in 3s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 5m55s
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dhv2-web",
|
||||
"version": "0.23.0-7",
|
||||
"version": "0.23.0-8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dhv2-web",
|
||||
"version": "0.23.0-7",
|
||||
"version": "0.23.0-8",
|
||||
"dependencies": {
|
||||
"maplibre-gl": "6.4.1",
|
||||
"react": "^19.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dhv2-web",
|
||||
"version": "0.23.0-7",
|
||||
"version": "0.23.0-8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const APP_VERSION = '0.23.0-7';
|
||||
export const APP_PHASE = 'F6.9 · Actas e informes consolidados';
|
||||
export const APP_VERSION = '0.23.0-8';
|
||||
export const APP_PHASE = 'F6.11 · GEDO y respuestas de informes';
|
||||
|
||||
@@ -64,6 +64,14 @@ export interface InspectionReportDetailF4 {
|
||||
companies: Array<{ id: string; code: string; name: string }>;
|
||||
areas: Array<{ id: string; code: string; name: string }>;
|
||||
findingCount: number;
|
||||
responseDueOn: string | null;
|
||||
deadlineReason: string | null;
|
||||
deadlines: Array<{ id: string; reportId: string | null; responseDueOn: string; reason: string; createdAt: string }>;
|
||||
findings: Array<{ id: string; code: string; title: string; status: string; assetCode: string; assetName: string; responseDueOn: string | null }>;
|
||||
companyResponses: Array<{
|
||||
id: string; reportId: string | null; receivedOn: string; details: string | null; committedCorrectionOn: string | null;
|
||||
contactName: string | null; contactEmail: string | null; originalName: string | null; sizeBytes: number | null; sha256: string | null; createdAt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PendingInspectionReportF4 {
|
||||
@@ -176,6 +184,30 @@ export function officializeInspectionReport(
|
||||
});
|
||||
}
|
||||
|
||||
export function setInspectionReportResponseDeadline(
|
||||
id: string,
|
||||
input: { responseDueOn: string; reason?: string | null },
|
||||
) {
|
||||
return apiRequest<InspectionReportWorkflowView>(`/inspection-reports/${id}/response-deadline`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function addInspectionReportCompanyResponse(
|
||||
id: string,
|
||||
input: { receivedOn: string; details?: string; committedCorrectionOn?: string; contactName?: string; contactEmail?: string; file?: File | null },
|
||||
) {
|
||||
const body = new FormData();
|
||||
body.set('receivedOn', input.receivedOn);
|
||||
if (input.details?.trim()) body.set('details', input.details.trim());
|
||||
if (input.committedCorrectionOn) body.set('committedCorrectionOn', input.committedCorrectionOn);
|
||||
if (input.contactName?.trim()) body.set('contactName', input.contactName.trim());
|
||||
if (input.contactEmail?.trim()) body.set('contactEmail', input.contactEmail.trim());
|
||||
if (input.file) body.set('file', input.file);
|
||||
return apiRequest<InspectionReportWorkflowView>(`/inspection-reports/${id}/company-responses`, { method: 'POST', body });
|
||||
}
|
||||
|
||||
export function addInspectionReportFollowUp(
|
||||
id: string,
|
||||
input: {
|
||||
@@ -206,6 +238,10 @@ export function inspectionReportGedoPdfDownloadUrl(id: string) {
|
||||
return `/api/v3/inspection-reports/${id}/gedo-pdf`;
|
||||
}
|
||||
|
||||
export function inspectionReportCompanyResponseDownloadUrl(reportId: string, responseId: string) {
|
||||
return `/api/v3/inspection-reports/${reportId}/company-responses/${responseId}/content`;
|
||||
}
|
||||
|
||||
export function inspectionReportFollowUpDownloadUrl(reportId: string, followUpId: string) {
|
||||
return `/api/v3/inspection-reports/${reportId}/follow-ups/${followUpId}/content`;
|
||||
}
|
||||
|
||||
@@ -2,21 +2,22 @@ import { useEffect, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { Link, useParams } from 'react-router';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { actCompanyResponseContentUrl, getActAdministration } from '../lib/api';
|
||||
import type { ActAdministrationDetail } from '../lib/api';
|
||||
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
||||
import { Icon } from '../components/Icon';
|
||||
import { SearchableSelect } from '../components/SearchableSelect';
|
||||
import { formatDate } from '../lib/format';
|
||||
import { formatDate, formatDateOnly } from '../lib/format';
|
||||
import {
|
||||
addInspectionReportCompanyResponse,
|
||||
addInspectionReportFollowUp,
|
||||
getInspectionReportF4,
|
||||
inspectionReportGedoPdfDownloadUrl,
|
||||
inspectionReportFollowUpDownloadUrl,
|
||||
inspectionReportCompanyResponseDownloadUrl,
|
||||
inspectionReportConsolidatedWordDownloadUrl,
|
||||
inspectionReportFollowUpDownloadUrl,
|
||||
inspectionReportGedoPdfDownloadUrl,
|
||||
inspectionReportWordDownloadUrl,
|
||||
listInspectionReportFollowUps,
|
||||
officializeInspectionReport,
|
||||
setInspectionReportResponseDeadline,
|
||||
updateInspectionReportNarrative,
|
||||
} from '../lib/reportWorkflowApi';
|
||||
import type {
|
||||
@@ -34,6 +35,11 @@ function localDateTime(value: Date): string {
|
||||
return local.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
function localDate(value = new Date()): string {
|
||||
const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000);
|
||||
return local.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function reportStatusLabel(status: InspectionReportDetailF4['status']): string {
|
||||
if (status === 'WORKING') return 'En preparación';
|
||||
if (status === 'OFFICIALIZED') return 'Oficializado en GEDO';
|
||||
@@ -66,10 +72,8 @@ export function ReportDetailPage() {
|
||||
const { id } = useParams();
|
||||
const { hasPermission } = useAuth();
|
||||
const canManage = hasPermission('inspection_reports.generate');
|
||||
const canReadActHistory = hasPermission('inspection_acts.read');
|
||||
const [report, setReport] = useState<InspectionReportDetailF4 | null>(null);
|
||||
const [followUps, setFollowUps] = useState<InspectionReportFollowUp[]>([]);
|
||||
const [legacy, setLegacy] = useState<ActAdministrationDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [working, setWorking] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -77,12 +81,21 @@ export function ReportDetailPage() {
|
||||
|
||||
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 [responseDueOn, setResponseDueOn] = useState('');
|
||||
const [deadlineReason, setDeadlineReason] = useState('');
|
||||
|
||||
const [companyReceivedOn, setCompanyReceivedOn] = useState(localDate());
|
||||
const [companyDetails, setCompanyDetails] = useState('');
|
||||
const [companyCommittedOn, setCompanyCommittedOn] = useState('');
|
||||
const [companyContactName, setCompanyContactName] = useState('');
|
||||
const [companyContactEmail, setCompanyContactEmail] = useState('');
|
||||
const [companyResponseFile, setCompanyResponseFile] = useState<File | null>(null);
|
||||
|
||||
const [followUpType, setFollowUpType] = useState<InspectionReportFollowUpType>('INTERNAL_NOTE');
|
||||
const [followUpOccurredAt, setFollowUpOccurredAt] = useState(localDateTime(new Date()));
|
||||
const [followUpReference, setFollowUpReference] = useState('');
|
||||
const [followUpDescription, setFollowUpDescription] = useState('');
|
||||
@@ -96,73 +109,80 @@ export function ReportDetailPage() {
|
||||
]);
|
||||
setReport(nextReport);
|
||||
setFollowUps(nextFollowUps);
|
||||
if (canReadActHistory) setLegacy(await getActAdministration(nextReport.actId).catch(() => null));
|
||||
setExecutiveSummary(nextReport.executiveSummary ?? '');
|
||||
setReportDescription(nextReport.reportDescription ?? '');
|
||||
setGedoIfIdentifier(nextReport.gedoIfIdentifier ?? '');
|
||||
if (nextReport.gedoOfficializedAt) {
|
||||
setGedoOfficializedAt(localDateTime(new Date(nextReport.gedoOfficializedAt)));
|
||||
}
|
||||
if (nextReport.gedoOfficializedAt) setGedoOfficializedAt(localDateTime(new Date(nextReport.gedoOfficializedAt)));
|
||||
setResponseDueOn(nextReport.responseDueOn?.slice(0, 10) ?? '');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
reload()
|
||||
.catch((requestError) => setError(errorMessage(requestError)))
|
||||
.finally(() => setLoading(false));
|
||||
reload().catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
const saveNarrative = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || report?.status !== 'WORKING') return;
|
||||
setWorking(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setWorking(true); setError(''); setSuccess('');
|
||||
try {
|
||||
await updateInspectionReportNarrative(id, {
|
||||
executiveSummary: executiveSummary.trim() || null,
|
||||
description: reportDescription.trim() || null,
|
||||
});
|
||||
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 {
|
||||
setWorking(false);
|
||||
}
|
||||
} catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); }
|
||||
};
|
||||
|
||||
const officialize = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || !gedoFile || !gedoIfIdentifier.trim() || !gedoOfficializedAt) return;
|
||||
setWorking(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setWorking(true); setError(''); setSuccess('');
|
||||
try {
|
||||
await officializeInspectionReport(id, {
|
||||
gedoIfIdentifier: gedoIfIdentifier.trim(),
|
||||
gedoOfficializedAt: new Date(gedoOfficializedAt).toISOString(),
|
||||
file: gedoFile,
|
||||
});
|
||||
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 {
|
||||
setWorking(false);
|
||||
}
|
||||
setSuccess('PDF oficial e identificador IF de GEDO cargados manualmente. El Informe quedó oficializado.');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); }
|
||||
};
|
||||
|
||||
const saveDeadline = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || report?.status !== 'OFFICIALIZED' || !responseDueOn) return;
|
||||
setWorking(true); setError(''); setSuccess('');
|
||||
try {
|
||||
await setInspectionReportResponseDeadline(id, { responseDueOn, reason: deadlineReason.trim() || null });
|
||||
setDeadlineReason('');
|
||||
await reload();
|
||||
setSuccess(`Vencimiento ${formatDateOnly(responseDueOn)} aplicado a todos los Hallazgos del Acta ${report.act.code}.`);
|
||||
} catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); }
|
||||
};
|
||||
|
||||
const addCompanyResponse = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || report?.status !== 'OFFICIALIZED' || !companyReceivedOn) return;
|
||||
if (!companyDetails.trim() && !companyResponseFile) return;
|
||||
setWorking(true); setError(''); setSuccess('');
|
||||
try {
|
||||
await addInspectionReportCompanyResponse(id, {
|
||||
receivedOn: companyReceivedOn,
|
||||
details: companyDetails.trim() || undefined,
|
||||
committedCorrectionOn: companyCommittedOn || undefined,
|
||||
contactName: companyContactName.trim() || undefined,
|
||||
contactEmail: companyContactEmail.trim() || undefined,
|
||||
file: companyResponseFile,
|
||||
});
|
||||
setCompanyDetails(''); setCompanyCommittedOn(''); setCompanyContactName(''); setCompanyContactEmail(''); setCompanyResponseFile(null); setCompanyReceivedOn(localDate());
|
||||
await reload();
|
||||
setSuccess(`Respuesta de empresa registrada y vinculada al Informe ${report.code} y al Acta ${report.act.code}.`);
|
||||
} catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); }
|
||||
};
|
||||
|
||||
const addFollowUp = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || !followUpOccurredAt) return;
|
||||
if (!followUpDescription.trim() && !followUpReference.trim() && !followUpFile) return;
|
||||
setWorking(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setWorking(true); setError(''); setSuccess('');
|
||||
try {
|
||||
const next = await addInspectionReportFollowUp(id, {
|
||||
type: followUpType,
|
||||
@@ -171,17 +191,9 @@ export function ReportDetailPage() {
|
||||
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 {
|
||||
setWorking(false);
|
||||
}
|
||||
setFollowUps(next); setFollowUpReference(''); setFollowUpDescription(''); setFollowUpFile(null); setFollowUpOccurredAt(localDateTime(new Date()));
|
||||
setSuccess('Antecedente agregado al historial del Informe.');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); } finally { setWorking(false); }
|
||||
};
|
||||
|
||||
if (loading) return <LoadingBlock label="Cargando informe…" />;
|
||||
@@ -190,105 +202,65 @@ export function ReportDetailPage() {
|
||||
{ id: 'act-start', date: report.act.occurredAt, title: `Inspección y Acta ${report.act.code}`, description: `${report.findingCount} hallazgo${report.findingCount === 1 ? '' : 's'} registrados`, href: `/inspecciones/actas/${report.actId}` },
|
||||
...(report.act.sealedAt ? [{ id: 'act-sealed', date: report.act.sealedAt, title: 'Acta firmada y cerrada', description: report.act.code, href: `/inspecciones/actas/${report.actId}` }] : []),
|
||||
{ id: 'report-issued', date: report.generatedAt, title: `Informe ${report.code} preparado`, description: 'Documento técnico vinculado al Acta' },
|
||||
...(report.gedoOfficializedAt ? [{ id: 'gedo', date: report.gedoOfficializedAt, title: 'Informe oficializado en GEDO', description: report.gedoIfIdentifier ?? '' }] : []),
|
||||
...(report.gedoOfficializedAt ? [{ id: 'gedo', date: report.gedoOfficializedAt, title: 'PDF oficial de GEDO cargado', description: report.gedoIfIdentifier ?? '' }] : []),
|
||||
...report.deadlines.map((item) => ({ id: `deadline-${item.id}`, date: item.createdAt, title: 'Vencimiento general del Acta', description: `${formatDateOnly(item.responseDueOn)} · ${item.reason}` })),
|
||||
...report.companyResponses.map((item) => ({ id: `response-${item.id}`, date: `${item.receivedOn}T12:00:00`, title: `Respuesta de empresa · Acta ${report.act.code}`, description: item.details ?? item.originalName ?? 'Respuesta registrada', href: item.originalName ? inspectionReportCompanyResponseDownloadUrl(report.id, item.id) : undefined, fileName: item.originalName ?? undefined })),
|
||||
...followUps.map((item) => ({ id: item.id, date: item.occurredAt, title: followUpLabel(item.type), description: item.description || item.externalReference || item.originalName || 'Antecedente registrado', href: item.originalName ? inspectionReportFollowUpDownloadUrl(report.id, item.id) : undefined, fileName: item.originalName ?? undefined })),
|
||||
...(legacy?.responses.map((item) => ({ id: `legacy-${item.id}`, date: item.receivedOn, title: 'Respuesta de empresa registrada previamente', description: item.details ?? 'Sin detalle', href: item.originalName ? actCompanyResponseContentUrl(item.id) : undefined, fileName: item.originalName ?? undefined })) ?? []),
|
||||
...(legacy?.deadlines.map((item) => ({ id: `deadline-${item.id}`, date: item.createdAt, title: 'Plazo administrativo registrado previamente', description: `${formatDate(item.responseDueOn)} · ${item.reason}` })) ?? []),
|
||||
].sort((a, b) => b.date.localeCompare(a.date)) : [];
|
||||
|
||||
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?.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>}
|
||||
{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 && <Alert type="success">{success}</Alert>}
|
||||
|
||||
{report && <>
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Un INF corresponde a una sola Acta.</strong> Una Inspección puede contener varias Actas y, por lo tanto, varios INF independientes. El Word puede editarse durante la preparación; el Acta sellada y sus Hallazgos permanecen inmutables.</p></div>
|
||||
<div className="temporal-notice"><Icon name="clipboard" /><p><strong>Un Informe corresponde a una sola Acta.</strong> GEDO no se consulta automáticamente: la oficialización se registra manualmente cargando el IF y su PDF oficial. Esa carga no genera una respuesta de empresa ni define un vencimiento por sí sola.</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 / Yacimiento</small><strong>{names(report.areas, 'Sin asignar')}</strong></div>
|
||||
<div><small>Hallazgos</small><strong>{report.findingCount}</strong></div>
|
||||
<div><small>Generado por</small><strong>{report.generatedBy.firstName} {report.generatedBy.lastName}</strong></div>
|
||||
</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 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">
|
||||
<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><div className="act-primary-actions"><a className="button secondary" href={inspectionReportConsolidatedWordDownloadUrl(report.id)}>Descargar Word del informe</a>{report.wordStatus === 'READY' && <a className="button secondary" href={inspectionReportWordDownloadUrl(report.id)}>Word anterior</a>}</div></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>
|
||||
<div className="responsible-summary"><div><small>Empresa</small><strong>{names(report.companies, '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>Generado por</small><strong>{report.generatedBy.firstName} {report.generatedBy.lastName}</strong></div></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 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">
|
||||
<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>
|
||||
{report.gedoPdfOriginalName && <div className="form-actions"><a className="button secondary" href={inspectionReportGedoPdfDownloadUrl(report.id)}>Descargar PDF oficial</a></div>}
|
||||
{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>}
|
||||
<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 antes de enviarlo a GEDO. Esta edición no altera el Acta fuente.</p></div><div className="act-primary-actions"><a className="button secondary" href={inspectionReportConsolidatedWordDownloadUrl(report.id)}>Descargar Word del informe</a>{report.wordStatus === 'READY' && <a className="button secondary" href={inspectionReportWordDownloadUrl(report.id)}>Word anterior</a>}</div></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'} /></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'} /></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 manualmente el PDF oficial de GEDO.</Alert>}</form>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">INFORME Y RESPUESTAS</span><h2>Historia y presentaciones</h2><p className="section-copy">La historia del Acta y las respuestas posteriores se leen en orden. Las nuevas respuestas se registran en este Informe.</p></div></div>
|
||||
<div className="dossier-link-list">{timeline.map((item) => <div key={item.id}>
|
||||
<div><strong>{item.title}</strong><small>{item.description}</small>{item.href && (item.fileName ? <a className="text-link" href={item.href}>Descargar {item.fileName}</a> : <Link className="text-link" to={item.href}>Ver Acta</Link>)}</div>
|
||||
<span>{formatDate(item.date)}</span>
|
||||
</div>)}</div>
|
||||
|
||||
{canManage && <form className="form-section" onSubmit={addFollowUp}>
|
||||
<div><h3>Registrar respuesta o antecedente</h3><p className="section-copy">La respuesta queda asociada a este Informe y conserva los registros 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>}
|
||||
<div className="panel-heading"><div><span className="eyebrow">GEDO</span><h2>Oficialización del Informe</h2><p className="section-copy">No existe una respuesta automática de GEDO. Cuando recibas el identificador IF y el PDF oficial, cargalos manualmente aquí.</p></div></div>
|
||||
{report.status === 'OFFICIALIZED' ? <><div className="responsible-summary"><div><small>Identificador IF</small><strong>{report.gedoIfIdentifier ?? '—'}</strong></div><div><small>Fecha GEDO</small><strong>{formatDate(report.gedoOfficializedAt)}</strong></div><div><small>PDF oficial</small><strong>{report.gedoPdfOriginalName ?? 'Registrado'}</strong></div><div><small>Vencimiento de respuestas</small><strong>{formatDateOnly(report.responseDueOn)}</strong></div></div>{report.gedoPdfOriginalName && <div className="form-actions"><a className="button secondary" href={inspectionReportGedoPdfDownloadUrl(report.id)}>Descargar PDF oficial</a></div>}{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 carga es manual. Registra la referencia institucional del Informe, pero no crea respuestas ni vencimientos automáticamente.</Alert><div className="form-actions"><button className="button primary" disabled={working || !gedoFile || !gedoIfIdentifier.trim()}>{working ? 'Registrando…' : 'Cargar IF y PDF oficial'}</button></div></form> : <Alert type="info">El Informe todavía no está oficializado en GEDO.</Alert>}
|
||||
</section>
|
||||
|
||||
<section className="panel report-integrity-panel">
|
||||
<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 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 className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">PLAZO DE RESPUESTA</span><h2>Vencimiento común del Acta</h2><p className="section-copy">La fecha se define una sola vez para el Acta {report.act.code} y se proyecta sobre todos sus Hallazgos.</p></div></div>
|
||||
{report.status !== 'OFFICIALIZED' ? <Alert type="info">Este paso se habilita después de cargar el PDF oficial de GEDO.</Alert> : <>
|
||||
<div className="responsible-summary"><div><small>Acta relacionada</small><strong>{report.act.code}</strong></div><div><small>Vencimiento vigente</small><strong>{formatDateOnly(report.responseDueOn)}</strong></div><div><small>Hallazgos alcanzados</small><strong>{report.findings.length}</strong></div><div><small>Motivo / referencia</small><strong>{report.deadlineReason ?? 'Sin definir'}</strong></div></div>
|
||||
{canManage && <form className="form-section" onSubmit={saveDeadline}><div className="form-grid"><label className="field"><span>Fecha de vencimiento</span><input type="date" value={responseDueOn} onChange={(event) => setResponseDueOn(event.target.value)} required /></label><label className="field"><span>Motivo / referencia <em>opcional</em></span><input value={deadlineReason} onChange={(event) => setDeadlineReason(event.target.value)} maxLength={1000} placeholder={`Vencimiento general del Acta ${report.act.code}`} /></label></div><div className="form-actions"><button className="button primary" disabled={working || !responseDueOn}>{report.responseDueOn ? 'Registrar nuevo vencimiento' : 'Definir vencimiento'}</button></div></form>}
|
||||
</>}
|
||||
<div className="table-scroll"><table><thead><tr><th>Hallazgo</th><th>Elemento</th><th>Estado</th><th>Vencimiento</th></tr></thead><tbody>{report.findings.map((finding) => <tr key={finding.id}><td><Link className="text-link" to={`/hallazgos/${finding.id}`}>{finding.code}</Link><small className="block-muted">{finding.title}</small></td><td><strong>{finding.assetName}</strong><small className="block-muted">{finding.assetCode}</small></td><td><span className={`status-badge ${finding.status === 'OPEN' ? 'observed' : 'active'}`}>{finding.status === 'OPEN' ? 'Abierto' : finding.status}</span></td><td><strong>{formatDateOnly(finding.responseDueOn)}</strong></td></tr>)}</tbody></table></div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">RESPUESTAS DE EMPRESA</span><h2>Presentaciones vinculadas al Acta {report.act.code}</h2><p className="section-copy">Cada respuesta queda vinculada simultáneamente a este Informe y a su Acta fuente. Puede incluir un PDF recibido de la empresa.</p></div></div>
|
||||
{report.status !== 'OFFICIALIZED' ? <Alert type="info">Las respuestas se habilitan después de cargar el PDF oficial de GEDO.</Alert> : <>
|
||||
{report.companyResponses.length === 0 ? <Alert type="info">Todavía no se registraron respuestas de la empresa.</Alert> : <div className="dossier-link-list">{report.companyResponses.map((item) => <div key={item.id}><div><strong>Respuesta recibida {formatDateOnly(item.receivedOn)}</strong><small>{item.details ?? 'Sin detalle'}{item.committedCorrectionOn ? ` · Compromiso: ${formatDateOnly(item.committedCorrectionOn)}` : ''}</small>{item.contactName && <small>{item.contactName}{item.contactEmail ? ` · ${item.contactEmail}` : ''}</small>}{item.originalName && <a className="text-link" href={inspectionReportCompanyResponseDownloadUrl(report.id, item.id)}>Descargar {item.originalName} {fileSize(item.sizeBytes) && `· ${fileSize(item.sizeBytes)}`}</a>}</div><span>Acta {report.act.code}</span></div>)}</div>}
|
||||
{canManage && <form className="form-section" onSubmit={addCompanyResponse}><div><h3>Registrar respuesta</h3><p className="section-copy">La respuesta se registra sobre el Informe {report.code} y queda relacionada con el Acta {report.act.code}.</p></div><div className="form-grid"><label className="field"><span>Fecha de recepción</span><input type="date" value={companyReceivedOn} onChange={(event) => setCompanyReceivedOn(event.target.value)} required /></label><label className="field"><span>Fecha comprometida por la empresa <em>opcional</em></span><input type="date" value={companyCommittedOn} onChange={(event) => setCompanyCommittedOn(event.target.value)} /></label><label className="field"><span>Contacto <em>opcional</em></span><input value={companyContactName} onChange={(event) => setCompanyContactName(event.target.value)} maxLength={200} /></label><label className="field"><span>Email <em>opcional</em></span><input type="email" value={companyContactEmail} onChange={(event) => setCompanyContactEmail(event.target.value)} maxLength={320} /></label></div><label className="field"><span>Detalle de la respuesta</span><textarea rows={5} maxLength={8000} value={companyDetails} onChange={(event) => setCompanyDetails(event.target.value)} placeholder="Respuesta, descargo, compromiso o documentación presentada…" /></label><label className="field"><span>PDF de respuesta <em>opcional</em></span><input type="file" accept="application/pdf,.pdf" onChange={(event) => setCompanyResponseFile(event.target.files?.[0] ?? null)} /></label><div className="form-actions"><button className="button primary" disabled={working || (!companyDetails.trim() && !companyResponseFile)}>Registrar respuesta</button></div></form>}
|
||||
</>}
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<div className="panel-heading"><div><span className="eyebrow">HISTORIA DEL INFORME</span><h2>Otros antecedentes</h2><p className="section-copy">Cronología documental completa. Las respuestas formales de empresa se cargan en el bloque anterior; aquí quedan notas internas, verificaciones y otros antecedentes.</p></div></div>
|
||||
<div className="dossier-link-list">{timeline.map((item) => <div key={item.id}><div><strong>{item.title}</strong><small>{item.description}</small>{item.href && (item.fileName ? <a className="text-link" href={item.href}>Descargar {item.fileName}</a> : <Link className="text-link" to={item.href}>Ver Acta</Link>)}</div><span>{formatDate(item.date)}</span></div>)}</div>
|
||||
{canManage && <form className="form-section" onSubmit={addFollowUp}><div><h3>Agregar otro antecedente</h3><p className="section-copy">No usar este bloque para respuestas formales de empresa.</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="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} /></label></div><label className="field"><span>Descripción</span><textarea rows={4} maxLength={20000} value={followUpDescription} onChange={(event) => setFollowUpDescription(event.target.value)} /></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)}>Agregar al historial</button></div></form>}
|
||||
</section>
|
||||
|
||||
<section className="panel report-integrity-panel"><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 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>
|
||||
</>}
|
||||
</section>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user