From 0db4567289edd87a69487c6056488f33ad7615d7 Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 21:31:57 -0300 Subject: [PATCH] F4: include editable INF fields in generated Word --- .../inspection-report-word.service.ts | 124 +++++++++++++----- 1 file changed, 90 insertions(+), 34 deletions(-) diff --git a/api-v3/src/inspection-reports/inspection-report-word.service.ts b/api-v3/src/inspection-reports/inspection-report-word.service.ts index 4074c81..be0a9dd 100644 --- a/api-v3/src/inspection-reports/inspection-report-word.service.ts +++ b/api-v3/src/inspection-reports/inspection-report-word.service.ts @@ -12,6 +12,8 @@ interface WordRow { id: string; code: string; title: string; + executiveSummary: string | null; + reportDescription: string | null; generatedAt: Date; frozenSha256: string; frozenSnapshot: Record; @@ -29,7 +31,8 @@ export class InspectionReportWordService { private readonly root: string; constructor(private readonly dataSource: DataSource, config: ConfigService) { - const configured = config.get('INSPECTION_REPORT_WORD_ROOT') ?? '/app/storage/asset-media/inspection-reports-word'; + const configured = config.get('INSPECTION_REPORT_WORD_ROOT') + ?? '/app/storage/asset-media/inspection-reports-word'; if (!isAbsolute(configured)) throw new Error('INSPECTION_REPORT_WORD_ROOT must be an absolute path'); this.root = resolve(configured); if (this.root === parse(this.root).root) throw new Error('INSPECTION_REPORT_WORD_ROOT cannot be the filesystem root'); @@ -38,7 +41,11 @@ export class InspectionReportWordService { async ensure(reportId: string): Promise { const row = await this.load(reportId); if (row.wordStatus === 'READY' && row.wordStoredName) { - try { await this.content(row.id); await this.ensureInitialRevision(row); return; } catch {} + try { + await this.content(row.id); + await this.ensureInitialRevision(row); + return; + } catch {} } try { const built = buildInspectionReportWord({ @@ -47,6 +54,8 @@ export class InspectionReportWordService { generatedAt: new Date(row.generatedAt), frozenSha256: row.frozenSha256, frozenSnapshot: row.frozenSnapshot, + executiveSummary: row.executiveSummary, + reportDescription: row.reportDescription, }); await mkdir(this.root, { recursive: true, mode: 0o700 }); const storedName = `${row.id}.docx`; @@ -55,32 +64,41 @@ export class InspectionReportWordService { await writeFile(filePath, built.buffer, { mode: 0o600 }); await this.dataSource.query(` UPDATE inspection_reports - SET word_status = 'READY', - word_original_name = $2, - word_stored_name = $3, - word_mime_type = $4, - word_size_bytes = $5, - word_sha256 = $6, - word_generated_at = CURRENT_TIMESTAMP, - word_error = NULL, - updated_at = CURRENT_TIMESTAMP - WHERE id = $1 + SET word_status='READY', + word_original_name=$2, + word_stored_name=$3, + word_mime_type=$4, + word_size_bytes=$5, + word_sha256=$6, + word_generated_at=CURRENT_TIMESTAMP, + word_error=NULL, + updated_at=CURRENT_TIMESTAMP + WHERE id=$1 `, [row.id, originalName, storedName, WORD_MIME, built.buffer.length, built.sha256]); await this.ensureInitialRevision(await this.load(row.id)); } catch (error) { const message = error instanceof Error ? error.message.slice(0, 500) : 'Error desconocido'; await this.dataSource.query(` UPDATE inspection_reports - SET word_status = 'FAILED', word_error = $2, updated_at = CURRENT_TIMESTAMP - WHERE id = $1 + SET word_status='FAILED',word_error=$2,updated_at=CURRENT_TIMESTAMP + WHERE id=$1 `, [row.id, message]).catch(() => undefined); } } async content(reportId: string): Promise<{ filePath: string; originalName: string; mimeType: string }> { const row = await this.load(reportId); - if (row.wordStatus !== 'READY' || !row.wordStoredName || !row.wordOriginalName || !row.wordSha256 || !row.wordSizeBytes) { - throw new NotFoundException({ code: 'INSPECTION_REPORT_WORD_NOT_READY', message: 'El archivo Word del informe todavía no está disponible' }); + if ( + row.wordStatus !== 'READY' + || !row.wordStoredName + || !row.wordOriginalName + || !row.wordSha256 + || !row.wordSizeBytes + ) { + throw new NotFoundException({ + code: 'INSPECTION_REPORT_WORD_NOT_READY', + message: 'El archivo Word del informe todavía no está disponible', + }); } const filePath = resolve(this.root, row.wordStoredName); if (!filePath.startsWith(`${this.root}/`)) throw this.storageError(); @@ -89,40 +107,78 @@ export class InspectionReportWordService { const buffer = await readFile(filePath); const sha256 = createHash('sha256').update(buffer).digest('hex'); if (sha256 !== row.wordSha256) throw this.storageError(); - return { filePath, originalName: row.wordOriginalName, mimeType: row.wordMimeType ?? WORD_MIME }; + return { + filePath, + originalName: row.wordOriginalName, + mimeType: row.wordMimeType ?? WORD_MIME, + }; } - private async ensureInitialRevision(row: WordRow): Promise { - if (row.wordStatus !== 'READY' || !row.wordOriginalName || !row.wordStoredName || !row.wordMimeType || !row.wordSizeBytes || !row.wordSha256) return; + if ( + row.wordStatus !== 'READY' + || !row.wordOriginalName + || !row.wordStoredName + || !row.wordMimeType + || !row.wordSizeBytes + || !row.wordSha256 + ) return; await this.dataSource.query(` INSERT INTO inspection_report_revisions ( - report_id, revision_number, source, original_name, stored_name, mime_type, - size_bytes, sha256, change_summary, created_by, created_at - ) VALUES ($1,1,'AUTO',$2,$3,$4,$5,$6,'Versión automática inicial',$7,COALESCE((SELECT word_generated_at FROM inspection_reports WHERE id=$1),CURRENT_TIMESTAMP)) - ON CONFLICT (report_id, revision_number) DO NOTHING - `, [row.id, row.wordOriginalName, row.wordStoredName, row.wordMimeType, row.wordSizeBytes, row.wordSha256, row.generatedBy]); + report_id,revision_number,source,original_name,stored_name,mime_type, + size_bytes,sha256,change_summary,created_by,created_at + ) VALUES ( + $1,1,'AUTO',$2,$3,$4,$5,$6,'Versión automática inicial editable',$7, + COALESCE((SELECT word_generated_at FROM inspection_reports WHERE id=$1),CURRENT_TIMESTAMP) + ) + ON CONFLICT (report_id,revision_number) DO NOTHING + `, [ + row.id, + row.wordOriginalName, + row.wordStoredName, + row.wordMimeType, + row.wordSizeBytes, + row.wordSha256, + row.generatedBy, + ]); await this.dataSource.query(` UPDATE inspection_reports - SET current_revision_number = GREATEST(current_revision_number, 1), updated_at = CURRENT_TIMESTAMP - WHERE id = $1 + SET current_revision_number=GREATEST(current_revision_number,1),updated_at=CURRENT_TIMESTAMP + WHERE id=$1 `, [row.id]); } private async load(reportId: string): Promise { const [row] = await this.dataSource.query(` - SELECT id, code, title, generated_at AS "generatedAt", frozen_sha256 AS "frozenSha256", - frozen_snapshot AS "frozenSnapshot", word_status AS "wordStatus", - word_original_name AS "wordOriginalName", word_stored_name AS "wordStoredName", - word_mime_type AS "wordMimeType", word_size_bytes::integer AS "wordSizeBytes", - word_sha256 AS "wordSha256", generated_by AS "generatedBy" - FROM inspection_reports WHERE id = $1 + SELECT id,code,title, + executive_summary AS "executiveSummary", + report_description AS "reportDescription", + generated_at AS "generatedAt", + frozen_sha256 AS "frozenSha256", + frozen_snapshot AS "frozenSnapshot", + word_status AS "wordStatus", + word_original_name AS "wordOriginalName", + word_stored_name AS "wordStoredName", + word_mime_type AS "wordMimeType", + word_size_bytes::integer AS "wordSizeBytes", + word_sha256 AS "wordSha256", + generated_by AS "generatedBy" + FROM inspection_reports + WHERE id=$1 `, [reportId]) as WordRow[]; - if (!row) throw new NotFoundException({ code: 'INSPECTION_REPORT_NOT_FOUND', message: 'Informe de inspección no encontrado' }); + if (!row) { + throw new NotFoundException({ + code: 'INSPECTION_REPORT_NOT_FOUND', + message: 'Informe de inspección no encontrado', + }); + } return row; } private storageError(): InternalServerErrorException { - return new InternalServerErrorException({ code: 'INSPECTION_REPORT_WORD_STORAGE_ERROR', message: 'El archivo Word del informe no está disponible o no supera la validación de integridad' }); + return new InternalServerErrorException({ + code: 'INSPECTION_REPORT_WORD_STORAGE_ERROR', + message: 'El archivo Word del informe no está disponible o no supera la validación de integridad', + }); } }