Files
dh-inspeccion-v2/api-v3/src/inspection-reports/inspection-act-pdf-builder.ts
T

213 lines
7.8 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 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[] {
const source = lockedSnapshot(snapshot);
const locked = source.locked;
const act = asRecord(locked.act);
const inspection = asRecord(act.inspection ?? asRecord(act).visit);
const responsible = asRecord(locked.responsible);
const inventories = asArray(locked.inventories ?? locked.assets);
const findings = asArray(locked.findings);
const signatures = source.signatures;
const companySignature = signatures.find((item) => text(item.signerType, '') === 'COMPANY_RESPONSIBLE');
const inspectorSignatures = signatures.filter((item) => text(item.signerType, '') === 'INSPECTOR');
const deadlineText = act.deadlineAt
? `${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: ${text(act.code)}`,
`Inspeccion: ${text(inspection.code)}`,
`Fecha: ${date(act.occurredAt)}`,
`Urgencia: ${urgencyLabel(act.urgency)}`,
`Plazo: ${deadlineText}`,
`Responsable empresa: ${text(responsible.fullName)}`,
`Cargo: ${text(responsible.position)}`,
'',
'RESUMEN',
...wrap(text(act.summary)),
'',
'OBSERVACIONES',
...wrap(text(act.observations)),
'',
'INVENTARIO INSPECCIONADO',
];
if (!inventories.length) out.push('-');
for (const item of inventories) {
out.push(...wrap(`${text(item.code)} | ${text(item.name)} | ${text(item.typeName ?? item.typeCode)}`));
}
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;
}
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 index = 0; index < all.length; index += 56) chunks.push(all.slice(index, index + 56));
if (!chunks.length) chunks.push(['ACTA DE INSPECCION']);
const pageCount = chunks.length;
const pageIds = Array.from({ length: pageCount }, (_, index) => 4 + index * 2);
const contentIds = Array.from({ length: pageCount }, (_, index) => 5 + index * 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, 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 position = header.length;
for (const object of objects) {
offsets.push(position);
position += object.length;
}
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') };
}