F3.1: unificar dossier cronológico tras merges
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AssetsService } from './assets.service';
|
||||
import { InventoryMergeService } from './inventory-merge.service';
|
||||
|
||||
type LooseRecord = Record<string, any>;
|
||||
|
||||
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,
|
||||
) {}
|
||||
|
||||
async dossier(requestedAssetId: string): Promise<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;
|
||||
return { assetId, identity, dossier };
|
||||
}));
|
||||
|
||||
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 timeline = dedupeById(
|
||||
dossiers.flatMap(({ identity, dossier }) =>
|
||||
((dossier.timeline ?? []) as LooseRecord[]).map((event) => ({
|
||||
...event,
|
||||
meta: {
|
||||
...(event.meta ?? {}),
|
||||
historicalInventory: {
|
||||
id: identity.id,
|
||||
code: identity.code,
|
||||
name: identity.name,
|
||||
isCanonical: identity.id === canonical.id,
|
||||
},
|
||||
},
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
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),
|
||||
},
|
||||
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,
|
||||
},
|
||||
visits,
|
||||
acts,
|
||||
findings,
|
||||
evidence,
|
||||
communications,
|
||||
verificationResults,
|
||||
documents,
|
||||
inspectionReports,
|
||||
reports,
|
||||
media,
|
||||
versions,
|
||||
timeline: timeline.slice(0, 1000),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user