243 lines
8.2 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|