Files
dh-inspeccion-v2/api-v3/src/act-administration/field-briefing.service.ts
T

243 lines
8.2 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { DataSource } from 'typeorm';
export type FieldBriefingActState =
| 'ACT_RESPONSE_OVERDUE'
| 'ACT_RESPONSE_DUE_SOON'
| 'WAITING_RESPONSE'
| 'COMPANY_COMMITMENT_OVERDUE'
| 'VERIFICATION_PENDING'
| 'RESPONSE_RECEIVED';
export interface FieldBriefingFinding {
id: string;
code: string;
title: string;
status: string;
severity: number | null;
nextControlOn: string | null;
asset: {
id: string;
code: string;
name: string;
typeName: string;
};
}
export interface FieldBriefingAct {
actId: string;
actCode: string;
occurredAt: Date;
adminState: FieldBriefingActState;
responseDueOn: string | null;
responseReceivedOn: string | null;
committedCorrectionOn: string | null;
latestResponseId: string | null;
findings: FieldBriefingFinding[];
}
@Injectable()
export class FieldBriefingService {
constructor(private readonly dataSource: DataSource) {}
async forVisit(visitId: string) {
const [visit] = (await this.dataSource.query(`
SELECT
visit.id,
visit.code,
visit.status,
visit.planned_start_at AS "plannedStartAt",
visit.operational_area_id AS "areaId",
visit.operator_company_id AS "companyId",
area.code AS "areaCode",
area.name AS "areaName",
company.code AS "companyCode",
COALESCE(profile.legal_name, company.name) AS "companyName"
FROM inspection_visits visit
LEFT JOIN assets area ON area.id = visit.operational_area_id
LEFT JOIN assets company ON company.id = visit.operator_company_id
LEFT JOIN organization_profiles profile ON profile.asset_id = company.id
WHERE visit.id = $1
`, [visitId])) as Array<Record<string, unknown>>;
if (!visit) {
throw new NotFoundException({
code: 'INSPECTION_VISIT_NOT_FOUND',
message: 'Inspección no encontrada',
});
}
const plannedOn = String(visit.plannedStartAt ?? new Date().toISOString()).slice(0, 10);
const rows = (await this.dataSource.query(`
WITH prior_acts AS (
SELECT
act.id AS act_id,
act.code AS act_code,
act.occurred_at,
deadline.response_due_on,
response.id AS latest_response_id,
response.received_on AS response_received_on,
response.committed_correction_on,
finding.id AS finding_id,
finding.code AS finding_code,
finding.title AS finding_title,
finding.status AS finding_status,
finding.severity,
finding.next_control_on,
asset.id AS asset_id,
asset.code AS asset_code,
asset.name AS asset_name,
asset_type.name AS asset_type_name,
CASE
WHEN response.id IS NULL
AND deadline.response_due_on IS NOT NULL
AND deadline.response_due_on < $4::date
THEN 'ACT_RESPONSE_OVERDUE'
WHEN response.id IS NULL
AND deadline.response_due_on IS NOT NULL
AND deadline.response_due_on BETWEEN $4::date AND ($4::date + 3)
THEN 'ACT_RESPONSE_DUE_SOON'
WHEN response.id IS NOT NULL
AND response.committed_correction_on IS NOT NULL
AND response.committed_correction_on < $4::date
THEN 'COMPANY_COMMITMENT_OVERDUE'
WHEN response.id IS NOT NULL
AND finding.next_control_on IS NULL
THEN 'VERIFICATION_PENDING'
WHEN response.id IS NOT NULL
THEN 'RESPONSE_RECEIVED'
ELSE 'WAITING_RESPONSE'
END AS admin_state
FROM inspection_acts act
INNER JOIN inspection_visits source_visit ON source_visit.id = act.visit_id
INNER JOIN inspection_findings finding ON finding.act_id = act.id
AND finding.status = 'OPEN'
INNER JOIN assets asset ON asset.id = finding.asset_id
INNER JOIN asset_types asset_type ON asset_type.id = asset.asset_type_id
LEFT JOIN LATERAL (
SELECT event.response_due_on
FROM inspection_act_deadline_events event
WHERE event.act_id = act.id
ORDER BY event.created_at DESC, event.id DESC
LIMIT 1
) deadline ON true
LEFT JOIN LATERAL (
SELECT company_response.id, company_response.received_on, company_response.committed_correction_on
FROM inspection_act_company_responses company_response
WHERE company_response.act_id = act.id
ORDER BY company_response.received_on DESC, company_response.created_at DESC, company_response.id DESC
LIMIT 1
) response ON true
WHERE act.status IN ('CLOSED', 'RECTIFIED')
AND source_visit.id <> $1
AND source_visit.operational_area_id = $2::uuid
AND source_visit.operator_company_id = $3::uuid
)
SELECT *
FROM prior_acts
WHERE admin_state IN (
'ACT_RESPONSE_OVERDUE',
'ACT_RESPONSE_DUE_SOON',
'COMPANY_COMMITMENT_OVERDUE',
'VERIFICATION_PENDING',
'RESPONSE_RECEIVED'
)
OR next_control_on IS NOT NULL
ORDER BY
CASE admin_state
WHEN 'ACT_RESPONSE_OVERDUE' THEN 1
WHEN 'COMPANY_COMMITMENT_OVERDUE' THEN 2
WHEN 'VERIFICATION_PENDING' THEN 3
WHEN 'ACT_RESPONSE_DUE_SOON' THEN 4
ELSE 5
END,
response_due_on NULLS LAST,
occurred_at,
act_code,
finding_code
`, [visitId, visit.areaId, visit.companyId, plannedOn])) as Array<{
act_id: string;
act_code: string;
occurred_at: Date;
response_due_on: string | null;
latest_response_id: string | null;
response_received_on: string | null;
committed_correction_on: string | null;
finding_id: string;
finding_code: string;
finding_title: string;
finding_status: string;
severity: number | null;
next_control_on: string | null;
asset_id: string;
asset_code: string;
asset_name: string;
asset_type_name: string;
admin_state: FieldBriefingActState;
}>;
const byAct = new Map<string, FieldBriefingAct>();
for (const row of rows) {
let act = byAct.get(row.act_id);
if (!act) {
act = {
actId: row.act_id,
actCode: row.act_code,
occurredAt: row.occurred_at,
adminState: row.admin_state,
responseDueOn: row.response_due_on,
responseReceivedOn: row.response_received_on,
committedCorrectionOn: row.committed_correction_on,
latestResponseId: row.latest_response_id,
findings: [],
};
byAct.set(row.act_id, act);
}
act.findings.push({
id: row.finding_id,
code: row.finding_code,
title: row.finding_title,
status: row.finding_status,
severity: row.severity,
nextControlOn: row.next_control_on,
asset: {
id: row.asset_id,
code: row.asset_code,
name: row.asset_name,
typeName: row.asset_type_name,
},
});
}
const acts = [...byAct.values()];
const assetIds = new Set<string>();
let findingCount = 0;
for (const act of acts) {
findingCount += act.findings.length;
for (const finding of act.findings) assetIds.add(finding.asset.id);
}
return {
inspection: {
id: visit.id,
code: visit.code,
status: visit.status,
plannedStartAt: visit.plannedStartAt,
area: visit.areaId ? { id: visit.areaId, code: visit.areaCode, name: visit.areaName } : null,
operatorCompany: visit.companyId ? { id: visit.companyId, code: visit.companyCode, name: visit.companyName } : null,
},
plannedOn,
summary: {
acts: acts.length,
findings: findingCount,
inventoryItems: assetIds.size,
responseOverdue: acts.filter((item) => item.adminState === 'ACT_RESPONSE_OVERDUE').length,
commitmentOverdue: acts.filter((item) => item.adminState === 'COMPANY_COMMITMENT_OVERDUE').length,
verificationPending: acts.filter((item) => item.adminState === 'VERIFICATION_PENDING').length,
},
acts,
};
}
}