Files
dh-inspeccion-v2/api-v3/src/inspection-reports/inspection-act-pdf-builder.ts
T
admin cd24736df8 F1.1 · Inspección multi-Acta y firma diferida
Implementa el nuevo núcleo operativo: múltiples Actas por Inspección, un Informe por Acta, cierre de campo independiente y firma de empresa diferida con conformidad/disidencia.
2026-09-05 14:46:26 -03:00

89 lines
6.1 KiB
TypeScript

import { createHash } from 'node:crypto';
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 out=String(value ?? '').trim(); return out || fallback; }
function date(value: unknown): string { const d=new Date(String(value ?? '')); return Number.isFinite(d.getTime()) ? d.toLocaleDateString('es-AR') : '-'; }
function clean(value: string): string { return value.normalize('NFKD').replace(/[\u0300-\u036f]/g,'').replace(/[–—]/g,'-').replace(/[“”]/g,'"').replace(/[‘’]/g,"'").replace(/[^\x20-\xFF]/g,'?'); }
function escapePdf(value: string): string { return clean(value).replaceAll('\\','\\\\').replaceAll('(','\\(').replaceAll(')','\\)'); }
function wrap(value: string, max=92): string[] { const words=clean(value).split(/\s+/).filter(Boolean); const out:string[]=[]; let line=''; for(const word of words){ const next=line?`${line} ${word}`:word; if(next.length>max&&line){out.push(line);line=word;}else line=next;} if(line)out.push(line); return out.length?out:['-']; }
function lines(snapshot: Record<string, unknown>): string[] {
const prepared=asRecord(asRecord(snapshot).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(snapshot.signatures);
const companySignature=signatures.find(item=>text(item.signerType,'')==='COMPANY_RESPONSIBLE');
const companies=[...new Set(assets.map(x=>text(asRecord(x.operatorCompany).name,'')).filter(Boolean))];
const areas=[...new Set(assets.map(x=>text(asRecord(x.operationalArea).name,'')).filter(Boolean))];
const inspectors=team.map(x=>`${text(x.firstName,'')} ${text(x.lastName,'')}`.trim()).filter(Boolean);
const out:string[]=[
'ACTA DE INSPECCION',
'Documento automatico - diseno institucional pendiente de definicion',
'',
`Acta: ${text(act.code)}`,
`Inspeccion: ${text(visit.code)}`,
`Fecha: ${date(act.occurredAt)}`,
`Empresa: ${companies.join(' / ') || 'Segun alcance del acta'}`,
`Area: ${areas.join(' / ') || 'Segun alcance del acta'}`,
`Inspector/es: ${inspectors.join(' / ') || '-'}`,
`Responsable empresa: ${text(responsible.fullName)}`,
`Cargo: ${text(responsible.position)}`,
'',
'OBJETIVO', ...wrap(text(visit.objective)), '',
'RESUMEN', ...wrap(text(act.summary)), '',
'OBSERVACIONES', ...wrap(text(act.observations)), '',
'ELEMENTOS INSPECCIONADOS',
];
if(!assets.length) out.push('-');
for(const item of assets) out.push(...wrap(`${text(item.code)} | ${text(item.name)}${text(item.commonName,'') ? ` | ${text(item.commonName,'')}` : ''} | ${text(item.typeName)}`));
out.push('', 'HALLAZGOS');
if(!findings.length) out.push('Sin hallazgos registrados.');
for(const item of findings){ out.push(...wrap(`${text(item.code)} | ${text(item.title)} | Vencimiento: ${date(item.correctionDueOn)}`)); out.push(...wrap(text(item.description))); }
out.push('', 'CONSTANCIA DE LA EMPRESA');
if(!companySignature) out.push('Firma o constancia pendiente.');
else if(text(companySignature.status,'')==='SIGNED'){
const manifestation=text(companySignature.companyManifestation,'CONFORMITY');
out.push(manifestation==='DISSENT'?'Firma en disidencia':'Firma en conformidad');
if(manifestation==='DISSENT') out.push(...wrap(text(companySignature.companyStatement)));
} else {
out.push(...wrap(`${text(companySignature.status)}: ${text(companySignature.reason)}`));
}
out.push('', 'INTEGRIDAD', `Hash de cierre: ${text(asRecord(snapshot).finalSha256 ?? asRecord(snapshot).preparedSha256)}`);
return out;
}
function objectBuffer(id: number, body: Buffer|string): Buffer { const data=Buffer.isBuffer(body)?body:Buffer.from(body,'latin1'); return Buffer.concat([Buffer.from(`${id} 0 obj\n`,'ascii'),data,Buffer.from('\nendobj\n','ascii')]); }
export function buildInspectionActPdf(snapshot: Record<string, unknown>): { buffer: Buffer; sha256: string } {
const all=lines(snapshot);
const chunks:Array<string[]>=[];
for(let i=0;i<all.length;i+=56) chunks.push(all.slice(i,i+56));
if(!chunks.length) chunks.push(['ACTA DE INSPECCION']);
const pageCount=chunks.length;
const pageIds=Array.from({length:pageCount},(_,i)=>4+i*2);
const contentIds=Array.from({length:pageCount},(_,i)=>5+i*2);
const objects:Buffer[]=[];
objects.push(objectBuffer(1,'<< /Type /Catalog /Pages 2 0 R >>'));
objects.push(objectBuffer(2,`<< /Type /Pages /Count ${pageCount} /Kids [${pageIds.map(id=>`${id} 0 R`).join(' ')}] >>`));
objects.push(objectBuffer(3,'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'));
chunks.forEach((chunk,index)=>{
const content=chunk.map((line,i)=>`${i===0?'':'T* '}(${escapePdf(line)}) Tj`).join('\n');
const stream=Buffer.from(`BT\n/F1 9 Tf\n40 800 Td\n12 TL\n${content}\nET`,'latin1');
objects.push(objectBuffer(pageIds[index]!,`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents ${contentIds[index]} 0 R >>`));
objects.push(objectBuffer(contentIds[index]!,Buffer.concat([Buffer.from(`<< /Length ${stream.length} >>\nstream\n`,'ascii'),stream,Buffer.from('\nendstream','ascii')])));
});
const header=Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n','binary');
const offsets:number[]=[0]; let pos=header.length;
for(const obj of objects){ offsets.push(pos); pos+=obj.length; }
const xrefOffset=pos;
const xref=[`xref\n0 ${objects.length+1}\n`,`0000000000 65535 f \n`,...objects.map((_,i)=>`${String(offsets[i+1]).padStart(10,'0')} 00000 n \n`)].join('');
const trailer=`trailer\n<< /Size ${objects.length+1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
const buffer=Buffer.concat([header,...objects,Buffer.from(xref+trailer,'ascii')]);
return { buffer, sha256:createHash('sha256').update(buffer).digest('hex') };
}