F4: rebuild Act PDF from locked and sealed snapshot
This commit is contained in:
@@ -1,88 +1,212 @@
|
|||||||
import { createHash } from 'node:crypto';
|
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 asRecord(value: unknown): Record<string, unknown> {
|
||||||
function asArray(value: unknown): Array<Record<string, unknown>> { return Array.isArray(value) ? value.map(asRecord) : []; }
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
function text(value: unknown, fallback='-'): string { const out=String(value ?? '').trim(); return out || fallback; }
|
? value as Record<string, unknown>
|
||||||
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 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 parsed = new Date(String(value ?? ''));
|
||||||
|
return Number.isFinite(parsed.getTime()) ? parsed.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 lockedSnapshot(snapshot: Record<string, unknown>): {
|
||||||
|
locked: Record<string, unknown>;
|
||||||
|
signatures: Array<Record<string, unknown>>;
|
||||||
|
finalSha256: unknown;
|
||||||
|
} {
|
||||||
|
const sealed = asRecord(snapshot);
|
||||||
|
const locked = asRecord(sealed.lockedSnapshot ?? sealed.preparedSnapshot);
|
||||||
|
return {
|
||||||
|
locked,
|
||||||
|
signatures: asArray(sealed.signatures),
|
||||||
|
finalSha256: sealed.finalSha256 ?? sealed.lockedSha256 ?? sealed.preparedSha256,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function urgencyLabel(value: unknown): string {
|
||||||
|
return text(value, '') === 'URGENT' ? 'Urgente' : 'No urgente';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayTypeLabel(value: unknown): string {
|
||||||
|
return text(value, '') === 'CALENDAR' ? 'dias corridos' : 'dias habiles';
|
||||||
|
}
|
||||||
|
|
||||||
function lines(snapshot: Record<string, unknown>): string[] {
|
function lines(snapshot: Record<string, unknown>): string[] {
|
||||||
const prepared=asRecord(asRecord(snapshot).preparedSnapshot);
|
const source = lockedSnapshot(snapshot);
|
||||||
const act=asRecord(prepared.act);
|
const locked = source.locked;
|
||||||
const visit=asRecord(act.visit);
|
const act = asRecord(locked.act);
|
||||||
const responsible=asRecord(prepared.responsible);
|
const inspection = asRecord(act.inspection ?? asRecord(act).visit);
|
||||||
const team=asArray(prepared.team);
|
const responsible = asRecord(locked.responsible);
|
||||||
const assets=asArray(prepared.assets);
|
const inventories = asArray(locked.inventories ?? locked.assets);
|
||||||
const findings=asArray(prepared.findings);
|
const findings = asArray(locked.findings);
|
||||||
const signatures=asArray(snapshot.signatures);
|
const signatures = source.signatures;
|
||||||
const companySignature=signatures.find(item=>text(item.signerType,'')==='COMPANY_RESPONSIBLE');
|
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 inspectorSignatures = signatures.filter((item) => text(item.signerType, '') === 'INSPECTOR');
|
||||||
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 deadlineText = act.deadlineAt
|
||||||
const out:string[]=[
|
? `${date(act.deadlineAt)} (${text(act.deadlineDays)} ${dayTypeLabel(act.deadlineDayType)})`
|
||||||
|
: act.deadlineBasis === 'GEDO_DATE'
|
||||||
|
? `${text(act.deadlineDays)} ${dayTypeLabel(act.deadlineDayType)} desde fecha GEDO`
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
const out: string[] = [
|
||||||
'ACTA DE INSPECCION',
|
'ACTA DE INSPECCION',
|
||||||
'Documento automatico - diseno institucional pendiente de definicion',
|
|
||||||
'',
|
'',
|
||||||
`Acta: ${text(act.code)}`,
|
`Acta: ${text(act.code)}`,
|
||||||
`Inspeccion: ${text(visit.code)}`,
|
`Inspeccion: ${text(inspection.code)}`,
|
||||||
`Fecha: ${date(act.occurredAt)}`,
|
`Fecha: ${date(act.occurredAt)}`,
|
||||||
`Empresa: ${companies.join(' / ') || 'Segun alcance del acta'}`,
|
`Urgencia: ${urgencyLabel(act.urgency)}`,
|
||||||
`Area: ${areas.join(' / ') || 'Segun alcance del acta'}`,
|
`Plazo: ${deadlineText}`,
|
||||||
`Inspector/es: ${inspectors.join(' / ') || '-'}`,
|
|
||||||
`Responsable empresa: ${text(responsible.fullName)}`,
|
`Responsable empresa: ${text(responsible.fullName)}`,
|
||||||
`Cargo: ${text(responsible.position)}`,
|
`Cargo: ${text(responsible.position)}`,
|
||||||
'',
|
'',
|
||||||
'OBJETIVO', ...wrap(text(visit.objective)), '',
|
'RESUMEN',
|
||||||
'RESUMEN', ...wrap(text(act.summary)), '',
|
...wrap(text(act.summary)),
|
||||||
'OBSERVACIONES', ...wrap(text(act.observations)), '',
|
'',
|
||||||
'ELEMENTOS INSPECCIONADOS',
|
'OBSERVACIONES',
|
||||||
|
...wrap(text(act.observations)),
|
||||||
|
'',
|
||||||
|
'INVENTARIO INSPECCIONADO',
|
||||||
];
|
];
|
||||||
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)}`));
|
if (!inventories.length) out.push('-');
|
||||||
out.push('', 'HALLAZGOS');
|
for (const item of inventories) {
|
||||||
if(!findings.length) out.push('Sin hallazgos registrados.');
|
out.push(...wrap(`${text(item.code)} | ${text(item.name)} | ${text(item.typeName ?? item.typeCode)}`));
|
||||||
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)}`);
|
|
||||||
|
out.push('', 'HALLAZGOS');
|
||||||
|
if (!findings.length) out.push('Sin hallazgos registrados.');
|
||||||
|
for (const item of findings) {
|
||||||
|
const recurrence = item.isRecurrence
|
||||||
|
? ` | REINCIDENCIA${item.recurrenceOfFindingId ? ` de ${text(item.recurrenceOfFindingCode ?? item.recurrenceOfFindingId)}` : ''}`
|
||||||
|
: '';
|
||||||
|
out.push(...wrap(`${text(item.code)} | ${text(item.title)}${recurrence}`));
|
||||||
|
out.push(...wrap(`Descripcion: ${text(item.description)}`));
|
||||||
|
if (item.legalBasis) out.push(...wrap(`Base legal: ${text(item.legalBasis)}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push('', 'FIRMAS Y CONSTANCIAS');
|
||||||
|
if (!inspectorSignatures.length) out.push('Firma de inspector: pendiente.');
|
||||||
|
for (const signature of inspectorSignatures) {
|
||||||
|
out.push(...wrap(`Inspector: ${text(signature.signerName)} | ${text(signature.status)} | ${date(signature.signedAt ?? signature.createdAt)}`));
|
||||||
|
}
|
||||||
|
if (!companySignature) {
|
||||||
|
out.push('Manifestacion de empresa: pendiente.');
|
||||||
|
} else if (text(companySignature.status, '') === 'SIGNED') {
|
||||||
|
const manifestation = text(companySignature.companyManifestation, 'CONFORMITY');
|
||||||
|
out.push(manifestation === 'DISSENT' ? 'Empresa: firma en disidencia' : 'Empresa: firma en conformidad');
|
||||||
|
if (manifestation === 'DISSENT') out.push(...wrap(text(companySignature.companyStatement)));
|
||||||
|
} else {
|
||||||
|
out.push(...wrap(`Empresa: ${text(companySignature.status)} - ${text(companySignature.reason)}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(
|
||||||
|
'',
|
||||||
|
'INTEGRIDAD',
|
||||||
|
`Hash del Acta sellada: ${text(source.finalSha256)}`,
|
||||||
|
`Hash del contenido bloqueado: ${text(snapshot.lockedSha256 ?? snapshot.preparedSha256)}`,
|
||||||
|
);
|
||||||
return out;
|
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')]); }
|
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 } {
|
export function buildInspectionActPdf(snapshot: Record<string, unknown>): { buffer: Buffer; sha256: string } {
|
||||||
const all=lines(snapshot);
|
const all = lines(snapshot);
|
||||||
const chunks:Array<string[]>=[];
|
const chunks: Array<string[]> = [];
|
||||||
for(let i=0;i<all.length;i+=56) chunks.push(all.slice(i,i+56));
|
for (let index = 0; index < all.length; index += 56) chunks.push(all.slice(index, index + 56));
|
||||||
if(!chunks.length) chunks.push(['ACTA DE INSPECCION']);
|
if (!chunks.length) chunks.push(['ACTA DE INSPECCION']);
|
||||||
const pageCount=chunks.length;
|
|
||||||
const pageIds=Array.from({length:pageCount},(_,i)=>4+i*2);
|
const pageCount = chunks.length;
|
||||||
const contentIds=Array.from({length:pageCount},(_,i)=>5+i*2);
|
const pageIds = Array.from({ length: pageCount }, (_, index) => 4 + index * 2);
|
||||||
const objects:Buffer[]=[];
|
const contentIds = Array.from({ length: pageCount }, (_, index) => 5 + index * 2);
|
||||||
objects.push(objectBuffer(1,'<< /Type /Catalog /Pages 2 0 R >>'));
|
const objects: Buffer[] = [];
|
||||||
objects.push(objectBuffer(2,`<< /Type /Pages /Count ${pageCount} /Kids [${pageIds.map(id=>`${id} 0 R`).join(' ')}] >>`));
|
objects.push(objectBuffer(1, '<< /Type /Catalog /Pages 2 0 R >>'));
|
||||||
objects.push(objectBuffer(3,'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'));
|
objects.push(objectBuffer(2, `<< /Type /Pages /Count ${pageCount} /Kids [${pageIds.map((id) => `${id} 0 R`).join(' ')}] >>`));
|
||||||
chunks.forEach((chunk,index)=>{
|
objects.push(objectBuffer(3, '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>'));
|
||||||
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');
|
chunks.forEach((chunk, index) => {
|
||||||
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 >>`));
|
const content = chunk
|
||||||
objects.push(objectBuffer(contentIds[index]!,Buffer.concat([Buffer.from(`<< /Length ${stream.length} >>\nstream\n`,'ascii'),stream,Buffer.from('\nendstream','ascii')])));
|
.map((line, lineIndex) => `${lineIndex === 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;
|
const header = Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'binary');
|
||||||
for(const obj of objects){ offsets.push(pos); pos+=obj.length; }
|
const offsets: number[] = [0];
|
||||||
const xrefOffset=pos;
|
let position = header.length;
|
||||||
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('');
|
for (const object of objects) {
|
||||||
const trailer=`trailer\n<< /Size ${objects.length+1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
|
offsets.push(position);
|
||||||
const buffer=Buffer.concat([header,...objects,Buffer.from(xref+trailer,'ascii')]);
|
position += object.length;
|
||||||
return { buffer, sha256:createHash('sha256').update(buffer).digest('hex') };
|
}
|
||||||
|
const xrefOffset = position;
|
||||||
|
const xref = [
|
||||||
|
`xref\n0 ${objects.length + 1}\n`,
|
||||||
|
'0000000000 65535 f \n',
|
||||||
|
...objects.map((_, index) => `${String(offsets[index + 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') };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user