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>; 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(); 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(); 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, }; } }