Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b86dbbc17 | ||
|
|
0279c22e20 | ||
|
|
2571bda8c4 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dhv2-api",
|
"name": "dhv2-api",
|
||||||
"version": "0.20.0-3",
|
"version": "0.20.0-2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AuditModule } from '../audit/audit.module';
|
import { AuditModule } from '../audit/audit.module';
|
||||||
import {
|
import { ActAdministrationController, ActAdministrationQueueController, ActCompanyResponseContentController } from './act-administration.controller';
|
||||||
ActAdministrationController,
|
|
||||||
ActAdministrationQueueController,
|
|
||||||
ActCompanyResponseContentController,
|
|
||||||
} from './act-administration.controller';
|
|
||||||
import { ActAdministrationService } from './act-administration.service';
|
import { ActAdministrationService } from './act-administration.service';
|
||||||
import { FieldBriefingController } from './field-briefing.controller';
|
|
||||||
import { FieldBriefingService } from './field-briefing.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuditModule],
|
imports: [AuditModule],
|
||||||
controllers: [
|
controllers: [ActAdministrationQueueController, ActAdministrationController, ActCompanyResponseContentController],
|
||||||
ActAdministrationQueueController,
|
providers: [ActAdministrationService],
|
||||||
ActAdministrationController,
|
|
||||||
ActCompanyResponseContentController,
|
|
||||||
FieldBriefingController,
|
|
||||||
],
|
|
||||||
providers: [ActAdministrationService, FieldBriefingService],
|
|
||||||
})
|
})
|
||||||
export class ActAdministrationModule {}
|
export class ActAdministrationModule {}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
|
|
||||||
import { RequirePermissions } from '../authorization/decorators/require-permissions.decorator';
|
|
||||||
import { FieldBriefingService } from './field-briefing.service';
|
|
||||||
|
|
||||||
@Controller('inspection-visits/:visitId/field-briefing')
|
|
||||||
export class FieldBriefingController {
|
|
||||||
constructor(private readonly briefing: FieldBriefingService) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@RequirePermissions('inspections.read')
|
|
||||||
get(@Param('visitId', new ParseUUIDPipe({ version: '4' })) visitId: string) {
|
|
||||||
return this.briefing.forVisit(visitId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export const API_VERSION = '0.20.0-3';
|
export const API_VERSION = '0.20.0-2';
|
||||||
export const API_PHASE = 'F1.3';
|
export const API_PHASE = 'F1.2';
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import assert from 'node:assert/strict';
|
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import { test } from 'node:test';
|
|
||||||
|
|
||||||
const service = readFileSync('src/act-administration/field-briefing.service.ts', 'utf8');
|
|
||||||
const controller = readFileSync('src/act-administration/field-briefing.controller.ts', 'utf8');
|
|
||||||
const moduleSource = readFileSync('src/act-administration/act-administration.module.ts', 'utf8');
|
|
||||||
|
|
||||||
test('F1.3 agrupa pendientes de campo por Acta y Hallazgo', () => {
|
|
||||||
assert.match(service, /inspection_act_deadline_events/);
|
|
||||||
assert.match(service, /inspection_act_company_responses/);
|
|
||||||
assert.match(service, /source_visit\.operational_area_id = \$2::uuid/);
|
|
||||||
assert.match(service, /source_visit\.operator_company_id = \$3::uuid/);
|
|
||||||
assert.match(service, /ACT_RESPONSE_OVERDUE/);
|
|
||||||
assert.match(service, /COMPANY_COMMITMENT_OVERDUE/);
|
|
||||||
assert.match(service, /VERIFICATION_PENDING/);
|
|
||||||
assert.match(service, /findings: \[\]/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('F1.3 expone el briefing para una inspección planificada', () => {
|
|
||||||
assert.match(controller, /inspection-visits\/:visitId\/field-briefing/);
|
|
||||||
assert.match(controller, /RequirePermissions\('inspections\.read'\)/);
|
|
||||||
assert.match(moduleSource, /FieldBriefingController/);
|
|
||||||
assert.match(moduleSource, /FieldBriefingService/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('F1.3 no usa el plazo del Hallazgo como plazo administrativo', () => {
|
|
||||||
assert.doesNotMatch(service, /correction_due_on/);
|
|
||||||
assert.match(service, /deadline\.response_due_on/);
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# F1.3 · Planificación inteligente de pendientes
|
|
||||||
|
|
||||||
Objetivo: al planificar una inspección para un Área/Yacimiento + Operadora, generar un paquete de campo con pendientes administrativos y operativos provenientes de Actas anteriores del mismo contexto.
|
|
||||||
|
|
||||||
Reglas:
|
|
||||||
- La unidad administrativa es el Acta.
|
|
||||||
- Los hallazgos se muestran dentro de su Acta de origen.
|
|
||||||
- El checklist de planificación incluye Actas con respuesta vencida o por vencer, respuestas de empresa con fecha comprometida de regularización, y hallazgos con control/verificación pendiente.
|
|
||||||
- El inspector puede ver antecedentes antes de salir y en la APK.
|
|
||||||
- La planificación no modifica ni cierra Actas/hallazgos previos.
|
|
||||||
- La verificación en campo se registra append-only.
|
|
||||||
Reference in New Issue
Block a user