chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
interface ZipEntry {
|
||||
name: string;
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
interface ReportWordInput {
|
||||
code: string;
|
||||
title: string;
|
||||
generatedAt: Date;
|
||||
frozenSha256: string;
|
||||
frozenSnapshot: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const crcTable = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n += 1) {
|
||||
let value = n;
|
||||
for (let k = 0; k < 8; k += 1) {
|
||||
value = (value & 1) ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
||||
}
|
||||
table[n] = value >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(data: Buffer): number {
|
||||
let crc = 0xffffffff;
|
||||
for (const byte of data) crc = crcTable[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function xmlEscape(value: unknown): string {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function asArray(value: unknown): Array<Record<string, unknown>> {
|
||||
return Array.isArray(value) ? value.map(asRecord) : [];
|
||||
}
|
||||
|
||||
function text(value: unknown, fallback = '—'): string {
|
||||
const normalized = String(value ?? '').trim();
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function isoDate(value: unknown): string {
|
||||
const date = new Date(String(value ?? ''));
|
||||
return Number.isFinite(date.getTime()) ? date.toLocaleDateString('es-AR') : '—';
|
||||
}
|
||||
|
||||
function paragraph(value: string, style?: 'Title' | 'Heading1' | 'Heading2'): string {
|
||||
const styleXml = style ? `<w:pPr><w:pStyle w:val="${style}"/></w:pPr>` : '';
|
||||
return `<w:p>${styleXml}<w:r><w:t xml:space="preserve">${xmlEscape(value)}</w:t></w:r></w:p>`;
|
||||
}
|
||||
|
||||
function labelValue(label: string, value: string): string {
|
||||
return `<w:p><w:r><w:rPr><w:b/></w:rPr><w:t>${xmlEscape(label)}: </w:t></w:r><w:r><w:t xml:space="preserve">${xmlEscape(value)}</w:t></w:r></w:p>`;
|
||||
}
|
||||
|
||||
function table(headers: string[], rows: string[][]): string {
|
||||
const cell = (value: string, bold = false) => `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr><w:p><w:r>${bold ? '<w:rPr><w:b/></w:rPr>' : ''}<w:t xml:space="preserve">${xmlEscape(value)}</w:t></w:r></w:p></w:tc>`;
|
||||
const header = `<w:tr>${headers.map((item) => cell(item, true)).join('')}</w:tr>`;
|
||||
const body = rows.map((row) => `<w:tr>${row.map((item) => cell(item)).join('')}</w:tr>`).join('');
|
||||
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 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 body = [
|
||||
paragraph('INFORME TÉCNICO DE INSPECCIÓN', 'Title'),
|
||||
paragraph('Borrador automático para revisión del Director de Hidrocarburos', 'Heading2'),
|
||||
labelValue('Informe', input.code),
|
||||
labelValue('Título', input.title),
|
||||
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('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.'),
|
||||
].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>`;
|
||||
}
|
||||
|
||||
function buildZip(entries: ZipEntry[]): Buffer {
|
||||
const locals: Buffer[] = [];
|
||||
const centrals: Buffer[] = [];
|
||||
let offset = 0;
|
||||
const dosDate = 33;
|
||||
const dosTime = 0;
|
||||
for (const entry of entries) {
|
||||
const name = Buffer.from(entry.name, 'utf8');
|
||||
const crc = crc32(entry.data);
|
||||
const local = Buffer.alloc(30);
|
||||
local.writeUInt32LE(0x04034b50, 0);
|
||||
local.writeUInt16LE(20, 4);
|
||||
local.writeUInt16LE(0, 6);
|
||||
local.writeUInt16LE(0, 8);
|
||||
local.writeUInt16LE(dosTime, 10);
|
||||
local.writeUInt16LE(dosDate, 12);
|
||||
local.writeUInt32LE(crc, 14);
|
||||
local.writeUInt32LE(entry.data.length, 18);
|
||||
local.writeUInt32LE(entry.data.length, 22);
|
||||
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);
|
||||
central.writeUInt16LE(20, 6);
|
||||
central.writeUInt16LE(0, 8);
|
||||
central.writeUInt16LE(0, 10);
|
||||
central.writeUInt16LE(dosTime, 12);
|
||||
central.writeUInt16LE(dosDate, 14);
|
||||
central.writeUInt32LE(crc, 16);
|
||||
central.writeUInt32LE(entry.data.length, 20);
|
||||
central.writeUInt32LE(entry.data.length, 24);
|
||||
central.writeUInt16LE(name.length, 28);
|
||||
central.writeUInt16LE(0, 30);
|
||||
central.writeUInt16LE(0, 32);
|
||||
central.writeUInt16LE(0, 34);
|
||||
central.writeUInt16LE(0, 36);
|
||||
central.writeUInt32LE(0, 38);
|
||||
central.writeUInt32LE(offset, 42);
|
||||
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);
|
||||
end.writeUInt16LE(0, 4);
|
||||
end.writeUInt16LE(0, 6);
|
||||
end.writeUInt16LE(entries.length, 8);
|
||||
end.writeUInt16LE(entries.length, 10);
|
||||
end.writeUInt32LE(centralData.length, 12);
|
||||
end.writeUInt32LE(offset, 16);
|
||||
end.writeUInt16LE(0, 20);
|
||||
return Buffer.concat([...locals, centralData, end]);
|
||||
}
|
||||
|
||||
export function buildInspectionReportWord(input: ReportWordInput): { buffer: Buffer; sha256: string } {
|
||||
const entries: ZipEntry[] = [
|
||||
{
|
||||
name: '[Content_Types].xml',
|
||||
data: Buffer.from('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>', 'utf8'),
|
||||
},
|
||||
{
|
||||
name: '_rels/.rels',
|
||||
data: Buffer.from('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>', 'utf8'),
|
||||
},
|
||||
{
|
||||
name: 'word/_rels/document.xml.rels',
|
||||
data: Buffer.from('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>', 'utf8'),
|
||||
},
|
||||
{
|
||||
name: 'word/styles.xml',
|
||||
data: Buffer.from('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:rPr><w:sz w:val="20"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:rPr><w:b/><w:sz w:val="34"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:rPr><w:b/><w:sz w:val="28"/></w:rPr></w:style><w:style w:type="paragraph" w:styleId="Heading2"><w:name w:val="heading 2"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:rPr><w:b/><w:sz w:val="22"/></w:rPr></w:style></w:styles>', 'utf8'),
|
||||
},
|
||||
{ name: 'word/document.xml', data: Buffer.from(documentXml(input), 'utf8') },
|
||||
{
|
||||
name: 'docProps/core.xml',
|
||||
data: Buffer.from(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>${xmlEscape(input.title)}</dc:title><dc:creator>DH Inspección</dc:creator><cp:lastModifiedBy>DH Inspección</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">${input.generatedAt.toISOString()}</dcterms:created></cp:coreProperties>`, 'utf8'),
|
||||
},
|
||||
{
|
||||
name: 'docProps/app.xml',
|
||||
data: Buffer.from('<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>DH Inspección</Application></Properties>', 'utf8'),
|
||||
},
|
||||
];
|
||||
const buffer = buildZip(entries);
|
||||
return { buffer, sha256: createHash('sha256').update(buffer).digest('hex') };
|
||||
}
|
||||
Reference in New Issue
Block a user