fix(actas): keep sealed act content findings-only
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dhv2-api",
|
||||
"version": "0.29.0-9",
|
||||
"version": "0.29.0-10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dhv2-api",
|
||||
"version": "0.29.0-9",
|
||||
"version": "0.29.0-10",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dhv2-api",
|
||||
"version": "0.29.0-9",
|
||||
"version": "0.29.0-10",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
|
||||
@@ -264,7 +264,6 @@ export class CompanySignatureInviteService {
|
||||
const invite = await this.resolveToken(this.dataSource.manager, token, false);
|
||||
const locked = invite.lockedSnapshot ?? {};
|
||||
const act = this.record(locked.act);
|
||||
const inventories = this.records(locked.inventories);
|
||||
const findings = this.records(locked.findings).map((finding) => ({
|
||||
id: finding.id,
|
||||
code: finding.code,
|
||||
@@ -296,12 +295,6 @@ export class CompanySignatureInviteService {
|
||||
documentNumber: invite.recipientDocumentNumber,
|
||||
position: invite.recipientPosition,
|
||||
},
|
||||
inventories: inventories.map((inventory) => ({
|
||||
id: inventory.id,
|
||||
code: inventory.code,
|
||||
name: inventory.name,
|
||||
typeName: inventory.typeName ?? inventory.typeCode ?? null,
|
||||
})),
|
||||
findings,
|
||||
consent: REMOTE_COMPANY_CONSENT,
|
||||
allowedActions: ['SIGN_CONFORMITY', 'SIGN_DISSENT', 'REFUSE'],
|
||||
|
||||
@@ -929,6 +929,12 @@ export class InspectionClosingService {
|
||||
JOIN assets asset ON asset.id=link.asset_id
|
||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
||||
WHERE link.act_id=$1 AND link.included=true
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM inspection_findings finding
|
||||
WHERE finding.act_id=link.act_id
|
||||
AND finding.asset_id=asset.id
|
||||
AND finding.status<>'VOIDED'
|
||||
)
|
||||
ORDER BY asset.code,asset.id
|
||||
`, [actId]) as Array<Record<string, unknown>>;
|
||||
const findings = await manager.query(`
|
||||
|
||||
@@ -38,7 +38,6 @@ export async function buildInspectionActPdf(snapshot: Record<string, unknown>, i
|
||||
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(sealed.signatures);
|
||||
const hash = text(sealed.finalSha256 ?? sealed.lockedSha256);
|
||||
@@ -87,32 +86,19 @@ export async function buildInspectionActPdf(snapshot: Record<string, unknown>, i
|
||||
if (text(act.summary) && !isPlaceholder(act.summary)) body(act.summary);
|
||||
else body(`Se realizó la inspección ${text(inspection.code)}. El contenido constatado se detalla en los hallazgos registrados a continuación.`);
|
||||
if (act.observations) { label('Observaciones', act.observations); }
|
||||
if (inventories.length) {
|
||||
heading('Instalaciones inspeccionadas');
|
||||
for (const item of inventories) body(`${text(item.name)} (${text(item.code)}) · ${text(item.typeName ?? item.typeCode)}`);
|
||||
}
|
||||
heading('Hallazgos y fotografías');
|
||||
if (!findings.length) body('No se registraron hallazgos.');
|
||||
const shownAssetPhotos = new Set<string>();
|
||||
for (const finding of findings) {
|
||||
need(75);
|
||||
doc.font('body-bold').fillColor(blue).fontSize(11).text(`${text(finding.code)} · ${text(finding.title)}`);
|
||||
label('Descripción', finding.description);
|
||||
if (finding.legalBasis) label('Normativa consignada', finding.legalBasis);
|
||||
if (finding.severity != null) label('Gravedad', `${text(finding.severity)}/10`);
|
||||
for (const photo of images.filter((item) => item.findingId === text(finding.id))) image(photo, `Fotografía del hallazgo ${text(finding.code)}${photo.capturedAt ? ` · ${date(photo.capturedAt)}` : ''}`);
|
||||
for (const photo of images.filter((item) => item.assetId === text(finding.assetId))) {
|
||||
if (shownAssetPhotos.has(photo.id)) continue;
|
||||
shownAssetPhotos.add(photo.id);
|
||||
image(photo, `Fotografía de inventario ${text(photo.title)}${photo.capturedAt ? ` · ${date(photo.capturedAt)}` : ''}`);
|
||||
for (const photo of images.filter((item) => item.findingId === text(finding.id))) {
|
||||
image(photo, `Fotografía del hallazgo ${text(finding.code)}${photo.capturedAt ? ` · ${date(photo.capturedAt)}` : ''}`);
|
||||
}
|
||||
doc.moveDown(0.4);
|
||||
}
|
||||
const otherAssetPhotos = images.filter((item) => item.assetId && !shownAssetPhotos.has(item.id));
|
||||
if (otherAssetPhotos.length) {
|
||||
heading('Otras instalaciones inspeccionadas');
|
||||
for (const photo of otherAssetPhotos) image(photo, `Fotografía de inventario ${text(photo.title)}${photo.capturedAt ? ` · ${date(photo.capturedAt)}` : ''}`);
|
||||
}
|
||||
heading('Intervinientes y firmas');
|
||||
for (const signature of signatures) {
|
||||
const name = text(signature.signerName);
|
||||
|
||||
@@ -66,7 +66,7 @@ export class InspectionActPdfService {
|
||||
`, [actId]);
|
||||
try {
|
||||
const snapshot = { ...row.finalSnapshot, finalSha256: row.closureSha256 };
|
||||
const built = await buildInspectionActPdf(snapshot, await this.fieldImages(actId), await this.actContext(actId));
|
||||
const built = await buildInspectionActPdf(snapshot, await this.fieldImages(actId, false), await this.actContext(actId));
|
||||
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
||||
const storedName = `${actId}.pdf`;
|
||||
const originalName = `${row.code}.pdf`;
|
||||
@@ -113,7 +113,7 @@ export class InspectionActPdfService {
|
||||
message: 'El Acta debe estar firmada y sellada para generar el documento consolidado',
|
||||
});
|
||||
const snapshot = { ...row.finalSnapshot, finalSha256: row.closureSha256 };
|
||||
const built = await buildInspectionActPdf(snapshot, await this.fieldImages(actId), await this.actContext(actId));
|
||||
const built = await buildInspectionActPdf(snapshot, await this.fieldImages(actId, false), await this.actContext(actId));
|
||||
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
||||
const storedName = `${actId}-consolidado-${built.sha256.slice(0, 24)}.pdf`;
|
||||
const originalName = `${row.code}-consolidada.pdf`;
|
||||
@@ -176,7 +176,7 @@ export class InspectionActPdfService {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async fieldImages(actId: string): Promise<ActPdfImage[]> {
|
||||
async fieldImages(actId: string, includeAssetPhotos = true): Promise<ActPdfImage[]> {
|
||||
type ImageRow = { id: string; findingId?: string; assetId?: string; signerName?: string; title?: string; capturedAt?: Date; storedName: string; sha256: string; sizeBytes: number };
|
||||
const findings = await this.dataSource.query(`
|
||||
SELECT evidence.id, finding.id AS "findingId", evidence.title,
|
||||
@@ -189,7 +189,7 @@ export class InspectionActPdfService {
|
||||
AND evidence.created_at<=act.locked_at
|
||||
ORDER BY finding.finding_number,evidence.captured_at,evidence.id
|
||||
`, [actId]) as ImageRow[];
|
||||
const assets = await this.dataSource.query(`
|
||||
const assets = includeAssetPhotos ? await this.dataSource.query(`
|
||||
SELECT media.id,asset.id AS "assetId", asset.name AS title,
|
||||
capture.device_captured_at AS "capturedAt",media.stored_name AS "storedName",
|
||||
media.sha256,media.size_bytes AS "sizeBytes"
|
||||
@@ -201,7 +201,7 @@ export class InspectionActPdfService {
|
||||
JOIN asset_media media ON media.id=capture.media_id AND media.deleted_at IS NULL AND media.kind='PHOTO'
|
||||
WHERE act.id=$1 AND capture.created_at<=act.locked_at
|
||||
ORDER BY capture.device_captured_at,media.id
|
||||
`, [actId]) as ImageRow[];
|
||||
`, [actId]) as ImageRow[] : [];
|
||||
const signatures = await this.dataSource.query(`
|
||||
SELECT signature.id,signature.signer_name AS "signerName",signature.stored_name AS "storedName",
|
||||
signature.image_sha256 AS sha256,signature.size_bytes AS "sizeBytes"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const api = (path: string) => readFileSync(resolve(process.cwd(), 'src', path), 'utf8');
|
||||
const web = (path: string) => readFileSync(resolve(process.cwd(), '..', 'web-v2', 'src', path), 'utf8');
|
||||
|
||||
test('Acta PDF renders Hallazgos and finding evidence, never standalone Inventory or field photos', () => {
|
||||
const source = api('inspection-reports/inspection-act-pdf-builder.ts');
|
||||
assert.match(source, /heading\('Hallazgos y fotografías'\)/);
|
||||
assert.match(source, /item\.findingId === text\(finding\.id\)/);
|
||||
assert.doesNotMatch(source, /Instalaciones inspeccionadas/);
|
||||
assert.doesNotMatch(source, /Otras instalaciones inspeccionadas/);
|
||||
assert.doesNotMatch(source, /item\.assetId === text\(finding\.assetId\)/);
|
||||
});
|
||||
|
||||
test('Dashboard Acta shows only Hallazgos and evidence directly attached to them', () => {
|
||||
const media = web('features/inspections/InspectionActMediaPanel.tsx');
|
||||
const editor = web('pages/InspectionActEditorPage.tsx');
|
||||
assert.match(media, /listInspectionFindingEvidence/);
|
||||
assert.doesNotMatch(media, /listInspectionActFieldMedia/);
|
||||
assert.doesNotMatch(media, /getAssetMediaBlob/);
|
||||
assert.doesNotMatch(media, /Fotos de otras instalaciones/);
|
||||
assert.doesNotMatch(editor, /Instalaciones inspeccionadas/);
|
||||
});
|
||||
|
||||
test('Company signing view exposes Hallazgos as Acta content without a standalone Inventory list', () => {
|
||||
const service = api('inspection-closing/company-signature-invite.service.ts');
|
||||
const viewBody = service.slice(service.indexOf(' async view(token: string)'), service.indexOf(' async sign('));
|
||||
const page = web('pages/CompanySignaturePage.tsx');
|
||||
assert.match(viewBody, /findings/);
|
||||
assert.doesNotMatch(viewBody, /inventories/);
|
||||
assert.match(page, />Hallazgos</);
|
||||
assert.doesNotMatch(page, /Inventario inspeccionado/);
|
||||
assert.doesNotMatch(page, /view\.inventories/);
|
||||
});
|
||||
|
||||
test('Acta sealed snapshot excludes Inventory that has no Hallazgo and PDF skips standalone asset media', () => {
|
||||
const closing = api('inspection-closing/inspection-closing.service.ts');
|
||||
const pdfService = api('inspection-reports/inspection-act-pdf.service.ts');
|
||||
const snapshotBody = closing.slice(closing.indexOf(' private async buildLockedSnapshot('), closing.indexOf(' private async deadlinePolicy('));
|
||||
assert.match(snapshotBody, /EXISTS \(\s*SELECT 1 FROM inspection_findings finding/);
|
||||
assert.match(snapshotBody, /finding\.asset_id=asset\.id/);
|
||||
assert.match(snapshotBody, /finding\.status<>'VOIDED'/);
|
||||
assert.match(pdfService, /fieldImages\(actId, false\)/);
|
||||
assert.match(pdfService, /includeAssetPhotos \? await this\.dataSource\.query/);
|
||||
});
|
||||
@@ -94,12 +94,12 @@ test('F6.9 revision migration archives both first document versions before servi
|
||||
assert.match(migration, /PRIMARY KEY \(report_id,template_version\)/);
|
||||
});
|
||||
|
||||
test('F6.9 includes field photos from installations without their own findings', async () => {
|
||||
test('Acta ignores standalone field photos while the technical Informe remains independent', async () => {
|
||||
const other = await sharp(photo).resize(75).png().toBuffer();
|
||||
const unlinked = { id: 'photo-other-asset', assetId: 'asset-2', title: 'Otra instalación', sha256: digest(other), buffer: other };
|
||||
const withUnlinked = await buildInspectionActPdf(sealed, [evidence, unlinked]);
|
||||
const linkedOnly = await buildInspectionActPdf(sealed, [evidence]);
|
||||
assert.ok(withUnlinked.buffer.length > linkedOnly.buffer.length + 500);
|
||||
assert.ok(Math.abs(withUnlinked.buffer.length - linkedOnly.buffer.length) < 500);
|
||||
const word = buildInspectionReportWord({ code: 'INF-OTHER', title: 'Informe', generatedAt: new Date(),
|
||||
frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed }, photos: [evidence, unlinked] });
|
||||
assert.ok(word.buffer.includes(Buffer.from('OTRAS INSTALACIONES INSPECCIONADAS')));
|
||||
|
||||
Reference in New Issue
Block a user