refactor(f4): make INF editable and one-to-one with sealed Act
This commit is contained in:
@@ -5,7 +5,6 @@ import { AuditService } from '../audit/audit.service';
|
|||||||
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
|
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
|
||||||
import {
|
import {
|
||||||
AuditAction,
|
AuditAction,
|
||||||
DocumentSequenceType,
|
|
||||||
InspectionActStatus,
|
InspectionActStatus,
|
||||||
InspectionReportPdfStatus,
|
InspectionReportPdfStatus,
|
||||||
InspectionReportStatus,
|
InspectionReportStatus,
|
||||||
@@ -16,7 +15,7 @@ import type { ListInspectionReportsQueryDto } from './dto/list-inspection-report
|
|||||||
import { InspectionReportWordService } from './inspection-report-word.service';
|
import { InspectionReportWordService } from './inspection-report-word.service';
|
||||||
import { InspectionDocumentDeliveryService } from './inspection-document-delivery.service';
|
import { InspectionDocumentDeliveryService } from './inspection-document-delivery.service';
|
||||||
|
|
||||||
const REPORT_SCHEMA_VERSION = 'DH-INSPECTION-REPORT-V1';
|
const REPORT_SCHEMA_VERSION = 'DH-INSPECTION-INF-V4';
|
||||||
|
|
||||||
interface ContextAsset {
|
interface ContextAsset {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -32,20 +31,31 @@ export interface InspectionReportListItem {
|
|||||||
reportNumber: number;
|
reportNumber: number;
|
||||||
code: string;
|
code: string;
|
||||||
status: InspectionReportStatus;
|
status: InspectionReportStatus;
|
||||||
|
executiveSummary: string | null;
|
||||||
|
reportDescription: string | null;
|
||||||
|
gedoIfIdentifier: string | null;
|
||||||
|
gedoOfficializedAt: Date | null;
|
||||||
|
gedoPdfOriginalName: string | null;
|
||||||
|
gedoPdfSha256: string | null;
|
||||||
pdfStatus: InspectionReportPdfStatus;
|
pdfStatus: InspectionReportPdfStatus;
|
||||||
wordStatus: 'PENDING' | 'READY' | 'FAILED';
|
wordStatus: 'PENDING' | 'READY' | 'FAILED';
|
||||||
wordGeneratedAt: Date | null;
|
wordGeneratedAt: Date | null;
|
||||||
reviewStatus: 'PENDING_REVIEW' | 'APPROVED' | 'SIGNED';
|
|
||||||
currentRevisionNumber: number;
|
|
||||||
approvedAt: Date | null;
|
|
||||||
signedAt: Date | null;
|
|
||||||
title: string;
|
title: string;
|
||||||
actVersion: number;
|
actVersion: number;
|
||||||
actClosureSha256: string;
|
actClosureSha256: string;
|
||||||
frozenSha256: string;
|
frozenSha256: string;
|
||||||
generatedAt: Date;
|
generatedAt: Date;
|
||||||
generatedBy: { id: string; username: string; firstName: string; lastName: string };
|
generatedBy: { id: string; username: string; firstName: string; lastName: string };
|
||||||
act: { id: string; code: string; title: string; status: string; occurredAt: Date; closedAt: Date | null };
|
act: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
occurredAt: Date;
|
||||||
|
sealedAt: Date | null;
|
||||||
|
urgency: string;
|
||||||
|
deadlineAt: Date | null;
|
||||||
|
};
|
||||||
visit: { id: string; code: string; title: string; status: string };
|
visit: { id: string; code: string; title: string; status: string };
|
||||||
companies: ContextAsset[];
|
companies: ContextAsset[];
|
||||||
areas: ContextAsset[];
|
areas: ContextAsset[];
|
||||||
@@ -63,7 +73,7 @@ export interface PendingInspectionReportItem {
|
|||||||
actTitle: string;
|
actTitle: string;
|
||||||
actYear: number;
|
actYear: number;
|
||||||
occurredAt: Date;
|
occurredAt: Date;
|
||||||
closedAt: Date;
|
sealedAt: Date;
|
||||||
closureSha256: string;
|
closureSha256: string;
|
||||||
visitCode: string;
|
visitCode: string;
|
||||||
visitTitle: string;
|
visitTitle: string;
|
||||||
@@ -127,7 +137,7 @@ export class InspectionReportsService {
|
|||||||
|
|
||||||
async listPending(query: ListInspectionReportsQueryDto) {
|
async listPending(query: ListInspectionReportsQueryDto) {
|
||||||
const conditions = [
|
const conditions = [
|
||||||
`act.status = 'CLOSED'`,
|
`act.status = 'SEALED'`,
|
||||||
'report.id IS NULL',
|
'report.id IS NULL',
|
||||||
'act.closure_sha256 IS NOT NULL',
|
'act.closure_sha256 IS NOT NULL',
|
||||||
];
|
];
|
||||||
@@ -143,21 +153,6 @@ export class InspectionReportsService {
|
|||||||
OR act.title ILIKE ${search}
|
OR act.title ILIKE ${search}
|
||||||
OR visit.code ILIKE ${search}
|
OR visit.code ILIKE ${search}
|
||||||
OR visit.title ILIKE ${search}
|
OR visit.title ILIKE ${search}
|
||||||
OR EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM inspection_act_assets search_link
|
|
||||||
INNER JOIN assets search_asset ON search_asset.id = search_link.asset_id
|
|
||||||
LEFT JOIN assets search_company ON search_company.id = search_asset.operator_company_id
|
|
||||||
LEFT JOIN assets search_area ON search_area.id = search_asset.operational_area_id
|
|
||||||
WHERE search_link.act_id = act.id
|
|
||||||
AND search_link.included = true
|
|
||||||
AND (
|
|
||||||
search_asset.name ILIKE ${search}
|
|
||||||
OR search_asset.code ILIKE ${search}
|
|
||||||
OR search_company.name ILIKE ${search}
|
|
||||||
OR search_area.name ILIKE ${search}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)`);
|
)`);
|
||||||
}
|
}
|
||||||
if (query.year) conditions.push(`act.act_year = ${add(query.year)}`);
|
if (query.year) conditions.push(`act.act_year = ${add(query.year)}`);
|
||||||
@@ -196,7 +191,7 @@ export class InspectionReportsService {
|
|||||||
act.title AS "actTitle",
|
act.title AS "actTitle",
|
||||||
act.act_year AS "actYear",
|
act.act_year AS "actYear",
|
||||||
act.occurred_at AS "occurredAt",
|
act.occurred_at AS "occurredAt",
|
||||||
act.closed_at AS "closedAt",
|
act.sealed_at AS "sealedAt",
|
||||||
act.closure_sha256 AS "closureSha256",
|
act.closure_sha256 AS "closureSha256",
|
||||||
visit.code AS "visitCode",
|
visit.code AS "visitCode",
|
||||||
visit.title AS "visitTitle",
|
visit.title AS "visitTitle",
|
||||||
@@ -226,16 +221,7 @@ export class InspectionReportsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string): Promise<InspectionReportView> {
|
async get(id: string): Promise<InspectionReportView> {
|
||||||
const [report] = (await this.dataSource.query(
|
return this.getWithManager(this.dataSource.manager, id);
|
||||||
`${this.reportSelect('WHERE report.id = $1')}`,
|
|
||||||
[id],
|
|
||||||
)) as InspectionReportListItem[];
|
|
||||||
if (!report) throw reportNotFound();
|
|
||||||
const [snapshot] = (await this.dataSource.query(
|
|
||||||
'SELECT frozen_snapshot AS "frozenSnapshot" FROM inspection_reports WHERE id = $1',
|
|
||||||
[id],
|
|
||||||
)) as Array<{ frozenSnapshot: Record<string, unknown> }>;
|
|
||||||
return { ...report, frozenSnapshot: snapshot.frozenSnapshot };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(
|
async generate(
|
||||||
@@ -244,11 +230,17 @@ export class InspectionReportsService {
|
|||||||
request: RequestWithContext,
|
request: RequestWithContext,
|
||||||
): Promise<InspectionReportView> {
|
): Promise<InspectionReportView> {
|
||||||
assertMobileInspector(principal);
|
assertMobileInspector(principal);
|
||||||
const report = await this.dataSource.transaction(async (manager) => this.ensureFrozenReport(manager, actId, principal, request));
|
const report = await this.dataSource.transaction((manager) =>
|
||||||
|
this.ensureFrozenReport(manager, actId, principal, request),
|
||||||
|
);
|
||||||
await this.word.ensure(report.id);
|
await this.word.ensure(report.id);
|
||||||
return this.get(report.id);
|
return this.get(report.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conserva una copia interna inmutable del Acta sellada como fuente documental.
|
||||||
|
* El INF que se genera desde esa fuente permanece editable y versionable.
|
||||||
|
*/
|
||||||
async ensureFrozenReport(
|
async ensureFrozenReport(
|
||||||
manager: EntityManager,
|
manager: EntityManager,
|
||||||
actId: string,
|
actId: string,
|
||||||
@@ -260,11 +252,13 @@ export class InspectionReportsService {
|
|||||||
act.id,
|
act.id,
|
||||||
act.visit_id AS "visitId",
|
act.visit_id AS "visitId",
|
||||||
act.act_year AS "actYear",
|
act.act_year AS "actYear",
|
||||||
|
act.act_number AS "actNumber",
|
||||||
act.code,
|
act.code,
|
||||||
act.title,
|
act.title,
|
||||||
act.status,
|
act.status,
|
||||||
act.current_version AS "currentVersion",
|
act.current_version AS "currentVersion",
|
||||||
act.closure_sha256 AS "closureSha256",
|
act.closure_sha256 AS "closureSha256",
|
||||||
|
act.occurred_at AS "occurredAt",
|
||||||
visit.code AS "visitCode",
|
visit.code AS "visitCode",
|
||||||
visit.title AS "visitTitle"
|
visit.title AS "visitTitle"
|
||||||
FROM inspection_acts act
|
FROM inspection_acts act
|
||||||
@@ -275,19 +269,21 @@ export class InspectionReportsService {
|
|||||||
id: string;
|
id: string;
|
||||||
visitId: string;
|
visitId: string;
|
||||||
actYear: number;
|
actYear: number;
|
||||||
|
actNumber: number;
|
||||||
code: string;
|
code: string;
|
||||||
title: string;
|
title: string;
|
||||||
status: InspectionActStatus;
|
status: InspectionActStatus;
|
||||||
currentVersion: number;
|
currentVersion: number;
|
||||||
closureSha256: string | null;
|
closureSha256: string | null;
|
||||||
|
occurredAt: Date;
|
||||||
visitCode: string;
|
visitCode: string;
|
||||||
visitTitle: string;
|
visitTitle: string;
|
||||||
}>;
|
}>;
|
||||||
if (!act) throw actNotFound();
|
if (!act) throw actNotFound();
|
||||||
if (act.status !== InspectionActStatus.CLOSED || !act.closureSha256) {
|
if (act.status !== InspectionActStatus.SEALED || !act.closureSha256) {
|
||||||
throw new ConflictException({
|
throw new ConflictException({
|
||||||
code: 'INSPECTION_REPORT_ACT_NOT_CLOSED',
|
code: 'INSPECTION_REPORT_ACT_NOT_SEALED',
|
||||||
message: 'El informe sólo puede emitirse después del cierre definitivo del acta',
|
message: 'El INF sólo puede generarse después de sellar el acta',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await this.assertActorAssigned(manager, act.visitId, principal.userId);
|
await this.assertActorAssigned(manager, act.visitId, principal.userId);
|
||||||
@@ -296,6 +292,7 @@ export class InspectionReportsService {
|
|||||||
[actId],
|
[actId],
|
||||||
)) as Array<{ id: string }>;
|
)) as Array<{ id: string }>;
|
||||||
if (existing) return this.getWithManager(manager, existing.id);
|
if (existing) return this.getWithManager(manager, existing.id);
|
||||||
|
|
||||||
const [closure] = (await manager.query(`
|
const [closure] = (await manager.query(`
|
||||||
SELECT final_snapshot AS "finalSnapshot", final_sha256 AS "finalSha256"
|
SELECT final_snapshot AS "finalSnapshot", final_sha256 AS "finalSha256"
|
||||||
FROM inspection_act_closures
|
FROM inspection_act_closures
|
||||||
@@ -306,47 +303,35 @@ export class InspectionReportsService {
|
|||||||
}>;
|
}>;
|
||||||
if (!closure?.finalSnapshot || closure.finalSha256 !== act.closureSha256) {
|
if (!closure?.finalSnapshot || closure.finalSha256 !== act.closureSha256) {
|
||||||
throw new ConflictException({
|
throw new ConflictException({
|
||||||
code: 'INSPECTION_REPORT_CLOSURE_NOT_FROZEN',
|
code: 'INSPECTION_REPORT_ACT_SNAPSHOT_INVALID',
|
||||||
message: 'El cierre del acta no tiene una instantánea final válida para emitir el informe',
|
message: 'El acta sellada no tiene una instantánea final válida',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const reportNumber = await this.allocateNumber(manager, act.actYear);
|
|
||||||
if (reportNumber > 999999) {
|
const reportNumber = act.actNumber;
|
||||||
throw new ConflictException({
|
const code = act.code.replace(/^ACT-/, 'INF-');
|
||||||
code: 'INSPECTION_REPORT_SEQUENCE_EXHAUSTED',
|
|
||||||
message: 'La numeración anual de informes agotó su rango disponible',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const code = `INF-${act.actYear}-${String(reportNumber).padStart(6, '0')}`;
|
|
||||||
const generatedAt = new Date();
|
const generatedAt = new Date();
|
||||||
const title = `Informe técnico · ${act.title}`.slice(0, 220);
|
const title = `Informe de inspección · ${act.code}`.slice(0, 220);
|
||||||
const frozenSnapshot = {
|
const frozenSnapshot = {
|
||||||
schemaVersion: REPORT_SCHEMA_VERSION,
|
schemaVersion: REPORT_SCHEMA_VERSION,
|
||||||
report: {
|
source: {
|
||||||
code,
|
|
||||||
reportYear: act.actYear,
|
|
||||||
reportNumber,
|
|
||||||
generatedAt: generatedAt.toISOString(),
|
|
||||||
generatedBy: principal.userId,
|
|
||||||
generatedByUsername: principal.username,
|
|
||||||
visitId: act.visitId,
|
|
||||||
visitCode: act.visitCode,
|
|
||||||
visitTitle: act.visitTitle,
|
|
||||||
actId: act.id,
|
actId: act.id,
|
||||||
actCode: act.code,
|
actCode: act.code,
|
||||||
actVersion: act.currentVersion,
|
actVersion: act.currentVersion,
|
||||||
actClosureSha256: act.closureSha256,
|
actClosureSha256: act.closureSha256,
|
||||||
|
inspectionId: act.visitId,
|
||||||
|
inspectionCode: act.visitCode,
|
||||||
},
|
},
|
||||||
actClosure: closure.finalSnapshot,
|
sealedAct: closure.finalSnapshot,
|
||||||
};
|
};
|
||||||
const frozenSha256 = sha256CanonicalJson(frozenSnapshot);
|
const frozenSha256 = sha256CanonicalJson(frozenSnapshot);
|
||||||
const [created] = (await manager.query(`
|
const [created] = (await manager.query(`
|
||||||
INSERT INTO inspection_reports (
|
INSERT INTO inspection_reports (
|
||||||
visit_id,act_id,report_year,report_number,code,status,pdf_status,
|
visit_id,act_id,report_year,report_number,code,status,pdf_status,
|
||||||
title, act_version, act_closure_sha256, frozen_sha256, frozen_snapshot,
|
executive_summary,report_description,title,act_version,act_closure_sha256,
|
||||||
generated_at, generated_by
|
frozen_sha256,frozen_snapshot,generated_at,generated_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, 'FROZEN', 'PENDING', $6, $7, $8, $9, $10, $11, $12
|
$1,$2,$3,$4,$5,'WORKING','PENDING',NULL,NULL,$6,$7,$8,$9,$10,$11,$12
|
||||||
)
|
)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`, [
|
`, [
|
||||||
@@ -371,17 +356,20 @@ export class InspectionReportsService {
|
|||||||
afterData: {
|
afterData: {
|
||||||
code,
|
code,
|
||||||
actId: act.id,
|
actId: act.id,
|
||||||
visitId: act.visitId,
|
inspectionId: act.visitId,
|
||||||
actVersion: act.currentVersion,
|
actVersion: act.currentVersion,
|
||||||
frozenSha256,
|
frozenSha256,
|
||||||
pdfStatus: InspectionReportPdfStatus.PENDING,
|
status: InspectionReportStatus.WORKING,
|
||||||
},
|
},
|
||||||
}, manager);
|
}, manager);
|
||||||
return this.getWithManager(manager, created.id);
|
return this.getWithManager(manager, created.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureWordForAct(actId: string): Promise<void> {
|
async ensureWordForAct(actId: string): Promise<void> {
|
||||||
const [row] = await this.dataSource.query('SELECT id FROM inspection_reports WHERE act_id = $1', [actId]) as Array<{ id: string }>;
|
const [row] = await this.dataSource.query(
|
||||||
|
'SELECT id FROM inspection_reports WHERE act_id = $1',
|
||||||
|
[actId],
|
||||||
|
) as Array<{ id: string }>;
|
||||||
if (row) await this.word.ensure(row.id);
|
if (row) await this.word.ensure(row.id);
|
||||||
await this.delivery.dispatchForAct(actId).catch(() => undefined);
|
await this.delivery.dispatchForAct(actId).catch(() => undefined);
|
||||||
}
|
}
|
||||||
@@ -398,25 +386,11 @@ export class InspectionReportsService {
|
|||||||
conditions.push(`(
|
conditions.push(`(
|
||||||
report.code ILIKE ${search}
|
report.code ILIKE ${search}
|
||||||
OR report.title ILIKE ${search}
|
OR report.title ILIKE ${search}
|
||||||
|
OR report.gedo_if_identifier ILIKE ${search}
|
||||||
OR act.code ILIKE ${search}
|
OR act.code ILIKE ${search}
|
||||||
OR act.title ILIKE ${search}
|
OR act.title ILIKE ${search}
|
||||||
OR visit.code ILIKE ${search}
|
OR visit.code ILIKE ${search}
|
||||||
OR visit.title ILIKE ${search}
|
OR visit.title ILIKE ${search}
|
||||||
OR EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM inspection_act_assets search_link
|
|
||||||
INNER JOIN assets search_asset ON search_asset.id = search_link.asset_id
|
|
||||||
LEFT JOIN assets search_company ON search_company.id = search_asset.operator_company_id
|
|
||||||
LEFT JOIN assets search_area ON search_area.id = search_asset.operational_area_id
|
|
||||||
WHERE search_link.act_id = act.id
|
|
||||||
AND search_link.included = true
|
|
||||||
AND (
|
|
||||||
search_asset.name ILIKE ${search}
|
|
||||||
OR search_asset.code ILIKE ${search}
|
|
||||||
OR search_company.name ILIKE ${search}
|
|
||||||
OR search_area.name ILIKE ${search}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)`);
|
)`);
|
||||||
}
|
}
|
||||||
if (query.year) conditions.push(`report.report_year = ${add(query.year)}`);
|
if (query.year) conditions.push(`report.report_year = ${add(query.year)}`);
|
||||||
@@ -454,14 +428,10 @@ export class InspectionReportsService {
|
|||||||
return `
|
return `
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(
|
COALESCE(JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(
|
||||||
'id', company.id,
|
'id',company.id,'code',company.code,'name',company.name
|
||||||
'code', company.code,
|
|
||||||
'name', company.name
|
|
||||||
)) FILTER (WHERE company.id IS NOT NULL),'[]'::jsonb) AS companies,
|
)) FILTER (WHERE company.id IS NOT NULL),'[]'::jsonb) AS companies,
|
||||||
COALESCE(JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(
|
COALESCE(JSONB_AGG(DISTINCT JSONB_BUILD_OBJECT(
|
||||||
'id', area.id,
|
'id',area.id,'code',area.code,'name',area.name
|
||||||
'code', area.code,
|
|
||||||
'name', area.name
|
|
||||||
)) FILTER (WHERE area.id IS NOT NULL),'[]'::jsonb) AS areas
|
)) FILTER (WHERE area.id IS NOT NULL),'[]'::jsonb) AS areas
|
||||||
FROM inspection_act_assets context_link
|
FROM inspection_act_assets context_link
|
||||||
INNER JOIN assets context_asset ON context_asset.id=context_link.asset_id
|
INNER JOIN assets context_asset ON context_asset.id=context_link.asset_id
|
||||||
@@ -481,37 +451,31 @@ export class InspectionReportsService {
|
|||||||
report.report_number AS "reportNumber",
|
report.report_number AS "reportNumber",
|
||||||
report.code,
|
report.code,
|
||||||
report.status,
|
report.status,
|
||||||
|
report.executive_summary AS "executiveSummary",
|
||||||
|
report.report_description AS "reportDescription",
|
||||||
|
report.gedo_if_identifier AS "gedoIfIdentifier",
|
||||||
|
report.gedo_officialized_at AS "gedoOfficializedAt",
|
||||||
|
report.gedo_pdf_original_name AS "gedoPdfOriginalName",
|
||||||
|
report.gedo_pdf_sha256 AS "gedoPdfSha256",
|
||||||
report.pdf_status AS "pdfStatus",
|
report.pdf_status AS "pdfStatus",
|
||||||
report.word_status AS "wordStatus",
|
report.word_status AS "wordStatus",
|
||||||
report.word_generated_at AS "wordGeneratedAt",
|
report.word_generated_at AS "wordGeneratedAt",
|
||||||
report.review_status AS "reviewStatus",
|
|
||||||
report.current_revision_number AS "currentRevisionNumber",
|
|
||||||
report.approved_at AS "approvedAt",
|
|
||||||
report.signed_at AS "signedAt",
|
|
||||||
report.title,
|
report.title,
|
||||||
report.act_version AS "actVersion",
|
report.act_version AS "actVersion",
|
||||||
report.act_closure_sha256 AS "actClosureSha256",
|
report.act_closure_sha256 AS "actClosureSha256",
|
||||||
report.frozen_sha256 AS "frozenSha256",
|
report.frozen_sha256 AS "frozenSha256",
|
||||||
report.generated_at AS "generatedAt",
|
report.generated_at AS "generatedAt",
|
||||||
JSONB_BUILD_OBJECT(
|
JSONB_BUILD_OBJECT(
|
||||||
'id', generator.id,
|
'id',generator.id,'username',generator.username,
|
||||||
'username', generator.username,
|
'firstName',generator.first_name,'lastName',generator.last_name
|
||||||
'firstName', generator.first_name,
|
|
||||||
'lastName', generator.last_name
|
|
||||||
) AS "generatedBy",
|
) AS "generatedBy",
|
||||||
JSONB_BUILD_OBJECT(
|
JSONB_BUILD_OBJECT(
|
||||||
'id', act.id,
|
'id',act.id,'code',act.code,'title',act.title,'status',act.status,
|
||||||
'code', act.code,
|
'occurredAt',act.occurred_at,'sealedAt',act.sealed_at,
|
||||||
'title', act.title,
|
'urgency',act.urgency,'deadlineAt',act.deadline_at
|
||||||
'status', act.status,
|
|
||||||
'occurredAt', act.occurred_at,
|
|
||||||
'closedAt', act.closed_at
|
|
||||||
) AS act,
|
) AS act,
|
||||||
JSONB_BUILD_OBJECT(
|
JSONB_BUILD_OBJECT(
|
||||||
'id', visit.id,
|
'id',visit.id,'code',visit.code,'title',visit.title,'status',visit.status
|
||||||
'code', visit.code,
|
|
||||||
'title', visit.title,
|
|
||||||
'status', visit.status
|
|
||||||
) AS visit,
|
) AS visit,
|
||||||
COALESCE(context.companies,'[]'::jsonb) AS companies,
|
COALESCE(context.companies,'[]'::jsonb) AS companies,
|
||||||
COALESCE(context.areas,'[]'::jsonb) AS areas,
|
COALESCE(context.areas,'[]'::jsonb) AS areas,
|
||||||
@@ -522,8 +486,7 @@ export class InspectionReportsService {
|
|||||||
INNER JOIN users generator ON generator.id=report.generated_by
|
INNER JOIN users generator ON generator.id=report.generated_by
|
||||||
LEFT JOIN LATERAL (${this.contextSelect()}) context ON true
|
LEFT JOIN LATERAL (${this.contextSelect()}) context ON true
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
SELECT COUNT(*) AS total
|
SELECT COUNT(*) AS total FROM inspection_findings finding
|
||||||
FROM inspection_findings finding
|
|
||||||
WHERE finding.act_id=act.id AND finding.status<>'VOIDED'
|
WHERE finding.act_id=act.id AND finding.status<>'VOIDED'
|
||||||
) finding_count ON true
|
) finding_count ON true
|
||||||
${where}
|
${where}
|
||||||
@@ -543,29 +506,15 @@ export class InspectionReportsService {
|
|||||||
return { ...report, frozenSnapshot: snapshot.frozenSnapshot };
|
return { ...report, frozenSnapshot: snapshot.frozenSnapshot };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async allocateNumber(manager: EntityManager, year: number): Promise<number> {
|
|
||||||
const [row] = (await manager.query(`
|
|
||||||
INSERT INTO document_annual_sequences (document_type, year, last_number)
|
|
||||||
VALUES ($1, $2, 1)
|
|
||||||
ON CONFLICT (document_type, year) DO UPDATE SET
|
|
||||||
last_number = document_annual_sequences.last_number + 1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING last_number AS number
|
|
||||||
`, [DocumentSequenceType.REPORT, year])) as Array<{ number: number }>;
|
|
||||||
return Number(row.number);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async assertActorAssigned(manager: EntityManager, visitId: string, userId: string): Promise<void> {
|
private async assertActorAssigned(manager: EntityManager, visitId: string, userId: string): Promise<void> {
|
||||||
const rows = (await manager.query(`
|
const rows = (await manager.query(`
|
||||||
SELECT 1
|
SELECT 1 FROM inspection_visit_members
|
||||||
FROM inspection_visit_members
|
WHERE visit_id=$1 AND user_id=$2 AND included=true LIMIT 1
|
||||||
WHERE visit_id = $1 AND user_id = $2 AND included = true
|
|
||||||
LIMIT 1
|
|
||||||
`, [visitId, userId])) as unknown[];
|
`, [visitId, userId])) as unknown[];
|
||||||
if (rows.length === 0) {
|
if (!rows.length) {
|
||||||
throw new ForbiddenException({
|
throw new ForbiddenException({
|
||||||
code: 'INSPECTION_REPORT_ACTOR_NOT_ASSIGNED',
|
code: 'INSPECTION_REPORT_ACTOR_NOT_ASSIGNED',
|
||||||
message: 'Sólo un inspector asignado a la visita puede solicitar el informe',
|
message: 'Sólo un inspector asignado a la inspección puede solicitar el informe',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user