fix(F6.1): make F4 checklist classification read-only
This commit is contained in:
@@ -4,15 +4,25 @@ 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 type { CreateInspectionVisitDto } from './dto/create-inspection-visit.dto';
|
import type { CreateInspectionVisitDto } from './dto/create-inspection-visit.dto';
|
||||||
import type { ListInspectionVisitsQueryDto } from './dto/list-inspection-visits-query.dto';
|
import type { ListInspectionVisitsQueryDto } from './dto/list-inspection-visits-query.dto';
|
||||||
import type { InspectionVisitView } from './inspection-visits.service';
|
import type {
|
||||||
|
InspectionChecklistItemKind,
|
||||||
|
InspectionVisitView,
|
||||||
|
} from './inspection-visits.service';
|
||||||
import { InspectionVisitsService } from './inspection-visits.service';
|
import { InspectionVisitsService } from './inspection-visits.service';
|
||||||
|
|
||||||
|
type F4ChecklistClassification = {
|
||||||
|
findingId: string;
|
||||||
|
itemKind: InspectionChecklistItemKind;
|
||||||
|
referenceOn: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capa activa F4 sobre el servicio de Inspecciones.
|
* Capa activa F4 sobre el servicio de Inspecciones.
|
||||||
*
|
*
|
||||||
* Conserva las operaciones de planificación/equipo/Inventario ya probadas y aplica
|
* El checklist persistido es una generación histórica append-only. F4 necesita
|
||||||
* la clasificación técnica F4 del checklist. El contrato público de Inspección ya
|
* mostrar la clasificación vigente de controles, pero una lectura nunca debe
|
||||||
* no expone título independiente ni fecha prevista de fin.
|
* reescribir esa generación. La clasificación se deriva con SELECT y se aplica
|
||||||
|
* sólo a la vista devuelta por la API.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class F4InspectionVisitsService extends InspectionVisitsService {
|
export class F4InspectionVisitsService extends InspectionVisitsService {
|
||||||
@@ -32,8 +42,8 @@ export class F4InspectionVisitsService extends InspectionVisitsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async getById(id: string): Promise<InspectionVisitView> {
|
override async getById(id: string): Promise<InspectionVisitView> {
|
||||||
await this.reclassifyCurrentChecklist(id);
|
const visit = await super.getById(id);
|
||||||
return this.normalizeChecklist(await super.getById(id));
|
return this.normalizeChecklist(await this.classifyChecklistView(visit));
|
||||||
}
|
}
|
||||||
|
|
||||||
override async create(
|
override async create(
|
||||||
@@ -41,9 +51,8 @@ export class F4InspectionVisitsService extends InspectionVisitsService {
|
|||||||
principal: AuthPrincipal,
|
principal: AuthPrincipal,
|
||||||
request: RequestWithContext,
|
request: RequestWithContext,
|
||||||
): Promise<InspectionVisitView> {
|
): Promise<InspectionVisitView> {
|
||||||
const created = await super.create(dto, principal, request);
|
const visit = await super.create(dto, principal, request);
|
||||||
await this.reclassifyCurrentChecklist(created.id);
|
return this.normalizeChecklist(await this.classifyChecklistView(visit));
|
||||||
return this.normalizeChecklist(await super.getById(created.id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override async generateChecklist(
|
override async generateChecklist(
|
||||||
@@ -51,88 +60,106 @@ export class F4InspectionVisitsService extends InspectionVisitsService {
|
|||||||
principal: AuthPrincipal,
|
principal: AuthPrincipal,
|
||||||
request: RequestWithContext,
|
request: RequestWithContext,
|
||||||
): Promise<InspectionVisitView> {
|
): Promise<InspectionVisitView> {
|
||||||
await super.generateChecklist(id, principal, request);
|
const visit = await super.generateChecklist(id, principal, request);
|
||||||
await this.reclassifyCurrentChecklist(id);
|
return this.normalizeChecklist(await this.classifyChecklistView(visit));
|
||||||
return this.normalizeChecklist(await super.getById(id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeChecklist<T>(visit: T): T {
|
private normalizeChecklist<T>(visit: T): T {
|
||||||
if (!visit || typeof visit !== 'object' || !('checklist' in visit)) return visit;
|
if (!visit || typeof visit !== 'object' || !('checklist' in visit)) return visit;
|
||||||
const view = visit as T & InspectionVisitView;
|
const view = visit as T & InspectionVisitView;
|
||||||
|
const items = view.checklist.items.filter((item) => item.itemKind !== 'COMPANY_OVERDUE');
|
||||||
|
const actionableAssetIds = new Set(
|
||||||
|
items
|
||||||
|
.filter((item) => ['VERIFICATION_OVERDUE', 'UPCOMING_CONTROL'].includes(item.itemKind))
|
||||||
|
.map((item) => item.asset.id),
|
||||||
|
);
|
||||||
view.checklist = {
|
view.checklist = {
|
||||||
...view.checklist,
|
...view.checklist,
|
||||||
|
antecedents: items.filter((item) => item.itemKind === 'ANTECEDENT').length,
|
||||||
companyOverdue: 0,
|
companyOverdue: 0,
|
||||||
items: view.checklist.items.filter((item) => item.itemKind !== 'COMPANY_OVERDUE'),
|
verificationOverdue: items.filter((item) => item.itemKind === 'VERIFICATION_OVERDUE').length,
|
||||||
|
upcomingControls: items.filter((item) => item.itemKind === 'UPCOMING_CONTROL').length,
|
||||||
|
actionableAssets: actionableAssetIds.size,
|
||||||
|
items,
|
||||||
};
|
};
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reclassifyCurrentChecklist(visitId: string): Promise<void> {
|
/**
|
||||||
await this.f4DataSource.transaction(async (manager) => {
|
* Deriva el estado F4 actual de los ítems sin mutar la generación persistida.
|
||||||
const [visit] = await manager.query(`
|
* La fuente administrativa de empresa vive a nivel Acta; en este checklist de
|
||||||
SELECT checklist_generation AS generation, planned_start_at AS "plannedStartAt"
|
* campo sólo importan próximos controles y verificaciones no resueltas.
|
||||||
FROM inspection_visits
|
*/
|
||||||
WHERE id=$1
|
private async classifyChecklistView(visit: InspectionVisitView): Promise<InspectionVisitView> {
|
||||||
FOR UPDATE
|
if (!visit.plannedStartAt || visit.checklist.items.length === 0) return visit;
|
||||||
`, [visitId]) as Array<{ generation: number; plannedStartAt: Date | null }>;
|
const findingIds = [...new Set(visit.checklist.items.map((item) => item.findingId))];
|
||||||
if (!visit || Number(visit.generation) < 1 || !visit.plannedStartAt) return;
|
if (findingIds.length === 0) return visit;
|
||||||
|
const targetDate = new Date(visit.plannedStartAt).toISOString().slice(0, 10);
|
||||||
|
|
||||||
const targetDate = new Date(visit.plannedStartAt).toISOString().slice(0, 10);
|
const classifications = (await this.f4DataSource.query(`
|
||||||
await manager.query(`
|
SELECT
|
||||||
UPDATE inspection_visit_checklist_items item
|
finding.id AS "findingId",
|
||||||
SET
|
CASE
|
||||||
item_kind = CASE
|
WHEN finding.status='OPEN'
|
||||||
WHEN finding.status='OPEN'
|
AND finding.next_control_on IS NOT NULL
|
||||||
AND finding.next_control_on IS NOT NULL
|
AND finding.next_control_on < $2::date
|
||||||
AND finding.next_control_on < $3::date
|
AND COALESCE((
|
||||||
AND COALESCE((
|
SELECT verification.outcome
|
||||||
SELECT verification.outcome
|
FROM inspection_finding_verification_visits verification
|
||||||
FROM inspection_finding_verification_visits verification
|
WHERE verification.finding_id=finding.id
|
||||||
WHERE verification.finding_id=finding.id
|
AND verification.outcome IS NOT NULL
|
||||||
AND verification.outcome IS NOT NULL
|
ORDER BY verification.result_recorded_at DESC NULLS LAST,
|
||||||
ORDER BY verification.result_recorded_at DESC NULLS LAST,
|
verification.created_at DESC,
|
||||||
verification.created_at DESC,
|
verification.id DESC
|
||||||
verification.id DESC
|
LIMIT 1
|
||||||
LIMIT 1
|
), '') <> 'RESOLVED'
|
||||||
), '') <> 'RESOLVED'
|
THEN 'VERIFICATION_OVERDUE'
|
||||||
THEN 'VERIFICATION_OVERDUE'
|
WHEN finding.status='OPEN'
|
||||||
WHEN finding.status='OPEN'
|
AND finding.next_control_on IS NOT NULL
|
||||||
AND finding.next_control_on IS NOT NULL
|
AND finding.next_control_on BETWEEN $2::date AND ($2::date + 30)
|
||||||
AND finding.next_control_on BETWEEN $3::date AND ($3::date + 30)
|
AND COALESCE((
|
||||||
AND COALESCE((
|
SELECT verification.outcome
|
||||||
SELECT verification.outcome
|
FROM inspection_finding_verification_visits verification
|
||||||
FROM inspection_finding_verification_visits verification
|
WHERE verification.finding_id=finding.id
|
||||||
WHERE verification.finding_id=finding.id
|
AND verification.outcome IS NOT NULL
|
||||||
AND verification.outcome IS NOT NULL
|
ORDER BY verification.result_recorded_at DESC NULLS LAST,
|
||||||
ORDER BY verification.result_recorded_at DESC NULLS LAST,
|
verification.created_at DESC,
|
||||||
verification.created_at DESC,
|
verification.id DESC
|
||||||
verification.id DESC
|
LIMIT 1
|
||||||
LIMIT 1
|
), '') <> 'RESOLVED'
|
||||||
), '') <> 'RESOLVED'
|
THEN 'UPCOMING_CONTROL'
|
||||||
THEN 'UPCOMING_CONTROL'
|
ELSE 'ANTECEDENT'
|
||||||
ELSE 'ANTECEDENT'
|
END AS "itemKind",
|
||||||
END,
|
CASE
|
||||||
reference_on = CASE
|
WHEN finding.status='OPEN'
|
||||||
WHEN finding.status='OPEN'
|
AND finding.next_control_on IS NOT NULL
|
||||||
AND finding.next_control_on IS NOT NULL
|
AND COALESCE((
|
||||||
AND COALESCE((
|
SELECT verification.outcome
|
||||||
SELECT verification.outcome
|
FROM inspection_finding_verification_visits verification
|
||||||
FROM inspection_finding_verification_visits verification
|
WHERE verification.finding_id=finding.id
|
||||||
WHERE verification.finding_id=finding.id
|
AND verification.outcome IS NOT NULL
|
||||||
AND verification.outcome IS NOT NULL
|
ORDER BY verification.result_recorded_at DESC NULLS LAST,
|
||||||
ORDER BY verification.result_recorded_at DESC NULLS LAST,
|
verification.created_at DESC,
|
||||||
verification.created_at DESC,
|
verification.id DESC
|
||||||
verification.id DESC
|
LIMIT 1
|
||||||
LIMIT 1
|
), '') <> 'RESOLVED'
|
||||||
), '') <> 'RESOLVED'
|
THEN finding.next_control_on::text
|
||||||
THEN finding.next_control_on
|
ELSE NULL
|
||||||
ELSE NULL
|
END AS "referenceOn"
|
||||||
END
|
FROM inspection_findings finding
|
||||||
FROM inspection_findings finding
|
WHERE finding.id=ANY($1::uuid[])
|
||||||
WHERE item.visit_id=$1
|
`, [findingIds, targetDate])) as F4ChecklistClassification[];
|
||||||
AND item.generation_number=$2
|
|
||||||
AND finding.id=item.finding_id
|
const byFinding = new Map(classifications.map((item) => [item.findingId, item]));
|
||||||
`, [visitId, visit.generation, targetDate]);
|
visit.checklist = {
|
||||||
});
|
...visit.checklist,
|
||||||
|
items: visit.checklist.items.map((item) => {
|
||||||
|
const classification = byFinding.get(item.findingId);
|
||||||
|
return classification
|
||||||
|
? { ...item, itemKind: classification.itemKind, referenceOn: classification.referenceOn }
|
||||||
|
: item;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return visit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user