F4: rebuild editable INF Word from sealed Act snapshot

This commit is contained in:
2026-09-07 21:31:35 -03:00
parent f5ab6ce885
commit 53702009e3
@@ -11,6 +11,8 @@ interface ReportWordInput {
generatedAt: Date; generatedAt: Date;
frozenSha256: string; frozenSha256: string;
frozenSnapshot: Record<string, unknown>; frozenSnapshot: Record<string, unknown>;
executiveSummary?: string | null;
reportDescription?: string | null;
} }
const crcTable = (() => { const crcTable = (() => {
@@ -56,8 +58,8 @@ function text(value: unknown, fallback = '—'): string {
} }
function isoDate(value: unknown): string { function isoDate(value: unknown): string {
const date = new Date(String(value ?? '')); const parsed = new Date(String(value ?? ''));
return Number.isFinite(date.getTime()) ? date.toLocaleDateString('es-AR') : '—'; return Number.isFinite(parsed.getTime()) ? parsed.toLocaleDateString('es-AR') : '—';
} }
function paragraph(value: string, style?: 'Title' | 'Heading1' | 'Heading2'): string { function paragraph(value: string, style?: 'Title' | 'Heading1' | 'Heading2'): string {
@@ -76,52 +78,96 @@ function table(headers: string[], rows: string[][]): string {
return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="B7C9D6"/><w:left w:val="single" w:sz="4" w:color="B7C9D6"/><w:bottom w:val="single" w:sz="4" w:color="B7C9D6"/><w:right w:val="single" w:sz="4" w:color="B7C9D6"/><w:insideH w:val="single" w:sz="4" w:color="D8E1E8"/><w:insideV w:val="single" w:sz="4" w:color="D8E1E8"/></w:tblBorders></w:tblPr>${header}${body}</w:tbl>`; return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="B7C9D6"/><w:left w:val="single" w:sz="4" w:color="B7C9D6"/><w:bottom w:val="single" w:sz="4" w:color="B7C9D6"/><w:right w:val="single" w:sz="4" w:color="B7C9D6"/><w:insideH w:val="single" w:sz="4" w:color="D8E1E8"/><w:insideV w:val="single" w:sz="4" w:color="D8E1E8"/></w:tblBorders></w:tblPr>${header}${body}</w:tbl>`;
} }
function f4Snapshot(input: ReportWordInput) {
const source = asRecord(input.frozenSnapshot.source);
const sealedAct = asRecord(input.frozenSnapshot.sealedAct ?? input.frozenSnapshot.actClosure);
const locked = asRecord(sealedAct.lockedSnapshot ?? sealedAct.preparedSnapshot);
const act = asRecord(locked.act);
const inspection = asRecord(act.inspection ?? act.visit);
const responsible = asRecord(locked.responsible);
const inventories = asArray(locked.inventories ?? locked.assets);
const findings = asArray(locked.findings);
const signatures = asArray(sealedAct.signatures);
return { source, sealedAct, locked, act, inspection, responsible, inventories, findings, signatures };
}
function urgency(value: unknown): string {
return text(value, '') === 'URGENT' ? 'Urgente' : 'No urgente';
}
function deadline(act: Record<string, unknown>): string {
const type = text(act.deadlineDayType, '') === 'CALENDAR' ? 'días corridos' : 'días hábiles';
if (act.deadlineAt) return `${isoDate(act.deadlineAt)} · ${text(act.deadlineDays)} ${type}`;
if (text(act.deadlineBasis, '') === 'GEDO_DATE') return `${text(act.deadlineDays)} ${type} desde la fecha de oficialización GEDO`;
return '—';
}
function documentXml(input: ReportWordInput): string { function documentXml(input: ReportWordInput): string {
const report = asRecord(input.frozenSnapshot.report); const snapshot = f4Snapshot(input);
const actClosure = asRecord(input.frozenSnapshot.actClosure); const findingRows = snapshot.findings.map((item) => [
const prepared = asRecord(actClosure.preparedSnapshot); text(item.code),
const act = asRecord(prepared.act); text(item.title),
const visit = asRecord(act.visit); text(item.description),
const responsible = asRecord(prepared.responsible); item.isRecurrence ? `${item.recurrenceOfFindingCode ? ` · ${text(item.recurrenceOfFindingCode)}` : ''}` : 'No',
const team = asArray(prepared.team); item.severity == null ? '—' : `${text(item.severity)}/10`,
const assets = asArray(prepared.assets); ]);
const findings = asArray(prepared.findings); const inventoryRows = snapshot.inventories.map((item) => [
const signatures = asArray(actClosure.signatures); text(item.code),
const companies = [...new Set(assets.map((item) => text(asRecord(item.operatorCompany).name, '')).filter(Boolean))]; text(item.name),
const areas = [...new Set(assets.map((item) => text(asRecord(item.operationalArea).name, '')).filter(Boolean))]; text(item.typeName ?? item.typeCode),
const inspectorNames = team.map((member) => `${text(member.firstName, '')} ${text(member.lastName, '')}`.trim()).filter(Boolean); ]);
const assetRows = assets.map((item) => [text(item.code), text(item.name), text(item.commonName, ''), text(item.typeName)]); const signatureRows = snapshot.signatures.map((item) => [
const findingRows = findings.map((item) => [text(item.code), text(item.title), text(item.description), item.severity == null ? '—' : `${text(item.severity)}/10`, isoDate(item.correctionDueOn)]); text(item.signerType),
const signatureRows = signatures.map((item) => [text(item.signerType), text(item.signerName), text(item.status), isoDate(item.signedAt ?? item.createdAt)]); text(item.signerName),
text(item.status),
text(item.companyManifestation, ''),
isoDate(item.signedAt ?? item.createdAt),
]);
const executive = input.executiveSummary?.trim()
|| '[EDITAR] Incorporar aquí el resumen ejecutivo del informe.';
const description = input.reportDescription?.trim()
|| '[EDITAR] Incorporar aquí la descripción técnica, análisis y consideraciones del inspector.';
const body = [ const body = [
paragraph('INFORME TÉCNICO DE INSPECCIÓN', 'Title'), paragraph('INFORME TÉCNICO DE INSPECCIÓN', 'Title'),
paragraph('Borrador automático para revisión del Director de Hidrocarburos', 'Heading2'), paragraph('Documento Word editable para revisión del Inspector antes de su incorporación a GEDO', 'Heading2'),
labelValue('Informe', input.code), labelValue('Informe', input.code),
labelValue('Título', input.title), labelValue('Acta fuente', text(snapshot.source.actCode ?? snapshot.act.code)),
labelValue('Inspección', text(snapshot.source.inspectionCode ?? snapshot.inspection.code)),
labelValue('Fecha de inspección', isoDate(snapshot.act.occurredAt)),
labelValue('Urgencia', urgency(snapshot.act.urgency)),
labelValue('Plazo', deadline(snapshot.act)),
labelValue('Fecha de generación', input.generatedAt.toLocaleString('es-AR')), labelValue('Fecha de generación', input.generatedAt.toLocaleString('es-AR')),
labelValue('Acta', text(report.actCode ?? act.code)), paragraph('Resumen ejecutivo', 'Heading1'),
labelValue('Inspección', text(report.visitCode ?? visit.code)), paragraph(executive),
labelValue('Empresa', companies.join(' · ') || 'Según alcance del acta'), paragraph('Descripción / análisis técnico', 'Heading1'),
labelValue('Área', areas.join(' · ') || 'Según alcance del acta'), paragraph(description),
labelValue('Inspector/es', inspectorNames.join(' · ') || '—'), paragraph('Acta fuente · contenido inmutable', 'Heading1'),
labelValue('Responsable de empresa', text(responsible.fullName)), paragraph('El bloque siguiente reproduce información proveniente del Acta sellada. Debe conservarse sin alterar su sentido ni sustituir los Hallazgos originales.'),
labelValue('Cargo', text(responsible.position)), labelValue('Resumen del Acta', text(snapshot.act.summary)),
paragraph('Datos de la visita', 'Heading1'), labelValue('Observaciones del Acta', text(snapshot.act.observations)),
labelValue('Objetivo', text(visit.objective)), labelValue('Responsable de empresa', text(snapshot.responsible.fullName)),
labelValue('Fecha de inspección', isoDate(act.occurredAt)), labelValue('Cargo', text(snapshot.responsible.position)),
labelValue('Resumen', text(act.summary)), paragraph('Inventario inspeccionado', 'Heading1'),
labelValue('Observaciones', text(act.observations)), inventoryRows.length
paragraph('Elementos inspeccionados', 'Heading1'), ? table(['Código', 'Nombre', 'Tipo'], inventoryRows)
assetRows.length ? table(['Código', 'Nombre técnico', 'Nombre habitual', 'Tipo'], assetRows) : paragraph('No se registraron elementos en la instantánea.'), : paragraph('No se registraron elementos de Inventario en el Acta.'),
paragraph('Hallazgos', 'Heading1'), paragraph('Hallazgos', 'Heading1'),
findingRows.length ? table(['Código', 'Título', 'Descripción', 'Gravedad', 'Vencimiento'], findingRows) : paragraph('No se registraron hallazgos en la instantánea.'), findingRows.length
paragraph('Firmas y constancias', 'Heading1'), ? table(['Código', 'Título', 'Descripción', 'Reincidencia', 'Gravedad'], findingRows)
signatureRows.length ? table(['Tipo', 'Firmante', 'Estado', 'Fecha'], signatureRows) : paragraph('No se registraron firmas en la instantánea.'), : paragraph('El Acta no contiene Hallazgos.'),
paragraph('Integridad documental', 'Heading1'), paragraph('Firmas y manifestaciones', 'Heading1'),
labelValue('Hash del informe', input.frozenSha256), signatureRows.length
labelValue('Hash de cierre del acta', text(report.actClosureSha256)), ? table(['Tipo', 'Firmante', 'Estado', 'Manifestación', 'Fecha'], signatureRows)
paragraph('El contenido de este archivo fue generado desde la instantánea congelada del acta. El diseño institucional y la firma final del Director se incorporarán en la etapa de revisión correspondiente.'), : paragraph('No se registraron firmas en la instantánea sellada.'),
paragraph('Integridad de la fuente', 'Heading1'),
labelValue('Hash de la fuente del INF', input.frozenSha256),
labelValue('Hash del Acta sellada', text(snapshot.source.actClosureSha256 ?? snapshot.sealedAct.finalSha256)),
labelValue('Hash del contenido bloqueado', text(snapshot.sealedAct.lockedSha256)),
paragraph('Este INF permanece editable mientras está en preparación. La edición del informe no modifica el Acta fuente ni los Hallazgos contenidos en ella. La versión oficial será la que se registre posteriormente en GEDO con su identificador IF y PDF oficial.'),
].join(''); ].join('');
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body}<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1134" w:right="1134" w:bottom="1134" w:left="1134" w:header="708" w:footer="708" w:gutter="0"/></w:sectPr></w:body></w:document>`; return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body}<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1134" w:right="1134" w:bottom="1134" w:left="1134" w:header="708" w:footer="708" w:gutter="0"/></w:sectPr></w:body></w:document>`;
} }
@@ -147,6 +193,7 @@ function buildZip(entries: ZipEntry[]): Buffer {
local.writeUInt16LE(name.length, 26); local.writeUInt16LE(name.length, 26);
local.writeUInt16LE(0, 28); local.writeUInt16LE(0, 28);
locals.push(local, name, entry.data); locals.push(local, name, entry.data);
const central = Buffer.alloc(46); const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0); central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 4);
@@ -168,6 +215,7 @@ function buildZip(entries: ZipEntry[]): Buffer {
centrals.push(central, name); centrals.push(central, name);
offset += local.length + name.length + entry.data.length; offset += local.length + name.length + entry.data.length;
} }
const centralData = Buffer.concat(centrals); const centralData = Buffer.concat(centrals);
const end = Buffer.alloc(22); const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0); end.writeUInt32LE(0x06054b50, 0);