F4: rebuild editable INF Word from sealed Act snapshot
This commit is contained in:
@@ -11,6 +11,8 @@ interface ReportWordInput {
|
||||
generatedAt: Date;
|
||||
frozenSha256: string;
|
||||
frozenSnapshot: Record<string, unknown>;
|
||||
executiveSummary?: string | null;
|
||||
reportDescription?: string | null;
|
||||
}
|
||||
|
||||
const crcTable = (() => {
|
||||
@@ -56,8 +58,8 @@ function text(value: unknown, fallback = '—'): string {
|
||||
}
|
||||
|
||||
function isoDate(value: unknown): string {
|
||||
const date = new Date(String(value ?? ''));
|
||||
return Number.isFinite(date.getTime()) ? date.toLocaleDateString('es-AR') : '—';
|
||||
const parsed = new Date(String(value ?? ''));
|
||||
return Number.isFinite(parsed.getTime()) ? parsed.toLocaleDateString('es-AR') : '—';
|
||||
}
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
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 {
|
||||
const report = asRecord(input.frozenSnapshot.report);
|
||||
const actClosure = asRecord(input.frozenSnapshot.actClosure);
|
||||
const prepared = asRecord(actClosure.preparedSnapshot);
|
||||
const act = asRecord(prepared.act);
|
||||
const visit = asRecord(act.visit);
|
||||
const responsible = asRecord(prepared.responsible);
|
||||
const team = asArray(prepared.team);
|
||||
const assets = asArray(prepared.assets);
|
||||
const findings = asArray(prepared.findings);
|
||||
const signatures = asArray(actClosure.signatures);
|
||||
const companies = [...new Set(assets.map((item) => text(asRecord(item.operatorCompany).name, '')).filter(Boolean))];
|
||||
const areas = [...new Set(assets.map((item) => text(asRecord(item.operationalArea).name, '')).filter(Boolean))];
|
||||
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 findingRows = findings.map((item) => [text(item.code), text(item.title), text(item.description), item.severity == null ? '—' : `${text(item.severity)}/10`, isoDate(item.correctionDueOn)]);
|
||||
const signatureRows = signatures.map((item) => [text(item.signerType), text(item.signerName), text(item.status), isoDate(item.signedAt ?? item.createdAt)]);
|
||||
const snapshot = f4Snapshot(input);
|
||||
const findingRows = snapshot.findings.map((item) => [
|
||||
text(item.code),
|
||||
text(item.title),
|
||||
text(item.description),
|
||||
item.isRecurrence ? `Sí${item.recurrenceOfFindingCode ? ` · ${text(item.recurrenceOfFindingCode)}` : ''}` : 'No',
|
||||
item.severity == null ? '—' : `${text(item.severity)}/10`,
|
||||
]);
|
||||
const inventoryRows = snapshot.inventories.map((item) => [
|
||||
text(item.code),
|
||||
text(item.name),
|
||||
text(item.typeName ?? item.typeCode),
|
||||
]);
|
||||
const signatureRows = snapshot.signatures.map((item) => [
|
||||
text(item.signerType),
|
||||
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 = [
|
||||
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('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('Acta', text(report.actCode ?? act.code)),
|
||||
labelValue('Inspección', text(report.visitCode ?? visit.code)),
|
||||
labelValue('Empresa', companies.join(' · ') || 'Según alcance del acta'),
|
||||
labelValue('Área', areas.join(' · ') || 'Según alcance del acta'),
|
||||
labelValue('Inspector/es', inspectorNames.join(' · ') || '—'),
|
||||
labelValue('Responsable de empresa', text(responsible.fullName)),
|
||||
labelValue('Cargo', text(responsible.position)),
|
||||
paragraph('Datos de la visita', 'Heading1'),
|
||||
labelValue('Objetivo', text(visit.objective)),
|
||||
labelValue('Fecha de inspección', isoDate(act.occurredAt)),
|
||||
labelValue('Resumen', text(act.summary)),
|
||||
labelValue('Observaciones', text(act.observations)),
|
||||
paragraph('Elementos inspeccionados', 'Heading1'),
|
||||
assetRows.length ? table(['Código', 'Nombre técnico', 'Nombre habitual', 'Tipo'], assetRows) : paragraph('No se registraron elementos en la instantánea.'),
|
||||
paragraph('Resumen ejecutivo', 'Heading1'),
|
||||
paragraph(executive),
|
||||
paragraph('Descripción / análisis técnico', 'Heading1'),
|
||||
paragraph(description),
|
||||
paragraph('Acta fuente · contenido inmutable', 'Heading1'),
|
||||
paragraph('El bloque siguiente reproduce información proveniente del Acta sellada. Debe conservarse sin alterar su sentido ni sustituir los Hallazgos originales.'),
|
||||
labelValue('Resumen del Acta', text(snapshot.act.summary)),
|
||||
labelValue('Observaciones del Acta', text(snapshot.act.observations)),
|
||||
labelValue('Responsable de empresa', text(snapshot.responsible.fullName)),
|
||||
labelValue('Cargo', text(snapshot.responsible.position)),
|
||||
paragraph('Inventario inspeccionado', 'Heading1'),
|
||||
inventoryRows.length
|
||||
? table(['Código', 'Nombre', 'Tipo'], inventoryRows)
|
||||
: paragraph('No se registraron elementos de Inventario en el Acta.'),
|
||||
paragraph('Hallazgos', 'Heading1'),
|
||||
findingRows.length ? table(['Código', 'Título', 'Descripción', 'Gravedad', 'Vencimiento'], findingRows) : paragraph('No se registraron hallazgos en la instantánea.'),
|
||||
paragraph('Firmas y constancias', 'Heading1'),
|
||||
signatureRows.length ? table(['Tipo', 'Firmante', 'Estado', 'Fecha'], signatureRows) : paragraph('No se registraron firmas en la instantánea.'),
|
||||
paragraph('Integridad documental', 'Heading1'),
|
||||
labelValue('Hash del informe', input.frozenSha256),
|
||||
labelValue('Hash de cierre del acta', text(report.actClosureSha256)),
|
||||
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.'),
|
||||
findingRows.length
|
||||
? table(['Código', 'Título', 'Descripción', 'Reincidencia', 'Gravedad'], findingRows)
|
||||
: paragraph('El Acta no contiene Hallazgos.'),
|
||||
paragraph('Firmas y manifestaciones', 'Heading1'),
|
||||
signatureRows.length
|
||||
? table(['Tipo', 'Firmante', 'Estado', 'Manifestación', 'Fecha'], signatureRows)
|
||||
: 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('');
|
||||
|
||||
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(0, 28);
|
||||
locals.push(local, name, entry.data);
|
||||
|
||||
const central = Buffer.alloc(46);
|
||||
central.writeUInt32LE(0x02014b50, 0);
|
||||
central.writeUInt16LE(20, 4);
|
||||
@@ -168,6 +215,7 @@ function buildZip(entries: ZipEntry[]): Buffer {
|
||||
centrals.push(central, name);
|
||||
offset += local.length + name.length + entry.data.length;
|
||||
}
|
||||
|
||||
const centralData = Buffer.concat(centrals);
|
||||
const end = Buffer.alloc(22);
|
||||
end.writeUInt32LE(0x06054b50, 0);
|
||||
|
||||
Reference in New Issue
Block a user