212 lines
8.2 KiB
TypeScript
212 lines
8.2 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { AssetsService } from './assets.service';
|
|
import { InventoryFunctionService } from './inventory-function.service';
|
|
import { InventoryMergeService } from './inventory-merge.service';
|
|
|
|
type LooseRecord = Record<string, any>;
|
|
|
|
const MERGEABLE_DOSSIER_TYPES = new Set(['instalacion', 'subinstalacion']);
|
|
|
|
function dedupeById<T extends LooseRecord>(items: T[]): T[] {
|
|
const seen = new Set<string>();
|
|
const result: T[] = [];
|
|
for (const item of items) {
|
|
const id = String(item.id ?? '');
|
|
if (!id || seen.has(id)) continue;
|
|
seen.add(id);
|
|
result.push(item);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function sortDesc(items: LooseRecord[], fieldCandidates: string[]): LooseRecord[] {
|
|
return [...items].sort((a, b) => {
|
|
const first = fieldCandidates.map((field) => b[field]).find(Boolean) ?? 0;
|
|
const second = fieldCandidates.map((field) => a[field]).find(Boolean) ?? 0;
|
|
return new Date(String(first)).getTime() - new Date(String(second)).getTime();
|
|
});
|
|
}
|
|
|
|
@Injectable()
|
|
export class MergedInventoryDossierService {
|
|
constructor(
|
|
private readonly assets: AssetsService,
|
|
private readonly merges: InventoryMergeService,
|
|
private readonly functions: InventoryFunctionService,
|
|
) {}
|
|
|
|
async dossier(requestedAssetId: string): Promise<Record<string, unknown>> {
|
|
const requestedAsset = await this.assets.getById(requestedAssetId);
|
|
const requestedTypeCode = requestedAsset.type.code.trim().toLowerCase();
|
|
|
|
// La conciliación/fusión existe sólo para Instalaciones y Subinstalaciones.
|
|
// Los registros territoriales (Departamento, Área, Yacimiento) deben poder
|
|
// abrir su Actividad sin depender del subsistema de merge.
|
|
if (!MERGEABLE_DOSSIER_TYPES.has(requestedTypeCode)) {
|
|
return await this.assets.dossier(requestedAssetId) as Record<string, unknown>;
|
|
}
|
|
|
|
const mergeStatus = await this.merges.status(requestedAssetId) as LooseRecord;
|
|
const canonical = mergeStatus.canonical as LooseRecord;
|
|
const requested = mergeStatus.requested as LooseRecord;
|
|
const aliases = (mergeStatus.aliases ?? []) as LooseRecord[];
|
|
const inventoryIds = [canonical.id, ...aliases.map((alias) => alias.id)]
|
|
.filter(Boolean)
|
|
.map(String)
|
|
.filter((value, index, all) => all.indexOf(value) === index);
|
|
|
|
const dossiers = await Promise.all(inventoryIds.map(async (assetId) => {
|
|
const dossier = await this.assets.dossier(assetId) as LooseRecord;
|
|
const identity = dossier.asset as LooseRecord;
|
|
const functionDossier = await this.functions.getForAsset(assetId).catch(() => null) as LooseRecord | null;
|
|
return { assetId, identity, dossier, functionDossier };
|
|
}));
|
|
|
|
const enrich = (entry: LooseRecord, identity: LooseRecord): LooseRecord => ({
|
|
...entry,
|
|
historicalInventory: {
|
|
id: identity.id,
|
|
code: identity.code,
|
|
name: identity.name,
|
|
commonName: identity.commonName ?? null,
|
|
isCanonical: identity.id === canonical.id,
|
|
},
|
|
});
|
|
|
|
const collect = (key: string): LooseRecord[] => dedupeById(
|
|
dossiers.flatMap(({ identity, dossier }) =>
|
|
((dossier[key] ?? []) as LooseRecord[]).map((entry) => enrich(entry, identity)),
|
|
),
|
|
);
|
|
|
|
const visits = sortDesc(collect('visits'), ['actualStartedAt', 'plannedStartAt', 'createdAt']);
|
|
const acts = sortDesc(collect('acts'), ['occurredAt', 'createdAt']);
|
|
const findings = sortDesc(collect('findings'), ['createdAt', 'updatedAt']);
|
|
const evidence = sortDesc(collect('evidence'), ['capturedAt', 'createdAt']);
|
|
const communications = sortDesc(collect('communications'), ['occurredAt', 'createdAt']);
|
|
const verificationResults = sortDesc(collect('verificationResults'), ['verifiedAt', 'resultRecordedAt']);
|
|
const documents = sortDesc(collect('documents'), ['documentDate', 'linkedAt']);
|
|
const inspectionReports = sortDesc(collect('inspectionReports'), ['generatedAt']);
|
|
const media = sortDesc(collect('media'), ['capturedAt', 'createdAt']);
|
|
const versions = sortDesc(collect('versions'), ['occurredAt']);
|
|
|
|
const functionHistory = sortDesc(
|
|
dossiers.flatMap(({ identity, functionDossier }) =>
|
|
(((functionDossier?.history ?? []) as LooseRecord[]).map((entry) => enrich(entry, identity))),
|
|
),
|
|
['validFrom', 'createdAt'],
|
|
);
|
|
const canonicalFunctionDossier = dossiers.find((item) => item.assetId === canonical.id)?.functionDossier ?? null;
|
|
const currentFunction = canonicalFunctionDossier?.currentFunction ?? null;
|
|
|
|
const timelineSource: LooseRecord[] = dossiers.flatMap(({ identity, dossier }) =>
|
|
((dossier.timeline ?? []) as LooseRecord[]).map((event): LooseRecord => ({
|
|
...event,
|
|
meta: {
|
|
...(event.meta ?? {}),
|
|
historicalInventory: {
|
|
id: identity.id,
|
|
code: identity.code,
|
|
name: identity.name,
|
|
isCanonical: identity.id === canonical.id,
|
|
},
|
|
},
|
|
})),
|
|
);
|
|
const timeline: LooseRecord[] = dedupeById<LooseRecord>(timelineSource);
|
|
|
|
for (const assignment of functionHistory) {
|
|
timeline.push({
|
|
id: `function:${String(assignment.id)}`,
|
|
kind: 'FUNCTION_CHANGED',
|
|
occurredAt: assignment.validFrom,
|
|
title: `Cambio de función · ${String(assignment.functionName)}`,
|
|
description: assignment.reason ?? null,
|
|
meta: {
|
|
functionId: assignment.functionId,
|
|
functionCode: assignment.functionCode,
|
|
functionName: assignment.functionName,
|
|
validFrom: assignment.validFrom,
|
|
validUntil: assignment.validUntil,
|
|
changedByUsername: assignment.changedByUsername,
|
|
historicalInventory: assignment.historicalInventory,
|
|
},
|
|
});
|
|
}
|
|
|
|
for (const alias of aliases) {
|
|
timeline.push({
|
|
id: `merge:${String(alias.id)}:${String(alias.mergedAt)}`,
|
|
kind: 'MERGE',
|
|
occurredAt: alias.mergedAt,
|
|
title: `Inventario fusionado · ${String(alias.code)}`,
|
|
description: alias.reason,
|
|
meta: {
|
|
sourceAssetId: alias.id,
|
|
sourceCode: alias.code,
|
|
sourceName: alias.name,
|
|
canonicalAssetId: canonical.id,
|
|
canonicalCode: canonical.code,
|
|
canonicalName: canonical.name,
|
|
historyPreserved: true,
|
|
},
|
|
});
|
|
}
|
|
timeline.sort((a, b) => new Date(String(b.occurredAt ?? 0)).getTime() - new Date(String(a.occurredAt ?? 0)).getTime());
|
|
|
|
const reports = documents.filter((document) => document.documentType === 'TECHNICAL_REPORT');
|
|
const openFindings = findings.filter((finding) => finding.status === 'OPEN').length;
|
|
const closedFindings = findings.filter((finding) => finding.status === 'CLOSED').length;
|
|
|
|
return {
|
|
asset: {
|
|
id: canonical.id,
|
|
code: canonical.code,
|
|
name: canonical.name,
|
|
commonName: dossiers.find((item) => item.assetId === canonical.id)?.identity?.commonName ?? null,
|
|
currentFunction,
|
|
},
|
|
requestedAsset: {
|
|
id: requested.id,
|
|
code: requested.code,
|
|
name: requested.name,
|
|
},
|
|
merge: {
|
|
requestedWasMerged: requested.id !== canonical.id,
|
|
canonicalAssetId: canonical.id,
|
|
aliases,
|
|
chain: mergeStatus.chain ?? [],
|
|
aggregatedInventoryIds: inventoryIds,
|
|
historyPolicy: 'CHRONOLOGICAL_AGGREGATION_WITH_ORIGINAL_IDENTITY',
|
|
},
|
|
counters: {
|
|
inspections: visits.length,
|
|
acts: acts.length,
|
|
findings: findings.length,
|
|
verifications: verificationResults.length,
|
|
openFindings,
|
|
closedFindings,
|
|
evidence: evidence.length,
|
|
documents: documents.length + media.filter((item) => item.kind === 'DOCUMENT').length,
|
|
photos: evidence.filter((item) => item.kind === 'PHOTO').length + media.filter((item) => item.kind === 'PHOTO').length,
|
|
reports: reports.length + inspectionReports.length,
|
|
functionChanges: functionHistory.length,
|
|
},
|
|
currentFunction,
|
|
functionHistory,
|
|
visits,
|
|
acts,
|
|
findings,
|
|
evidence,
|
|
communications,
|
|
verificationResults,
|
|
documents,
|
|
inspectionReports,
|
|
reports,
|
|
media,
|
|
versions,
|
|
timeline: timeline.slice(0, 1000),
|
|
};
|
|
}
|
|
}
|