Simplifica el flujo operativo móvil, corrige el alta desde el Yacimiento congelado, permite crear Actas antes de seleccionar Inventario y precarga Marca, Modelo, Capacidad, Número de serie y Función en Instalaciones/Subinstalaciones. Android 0.16.0.
236 lines
8.8 KiB
TypeScript
236 lines
8.8 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
Injectable,
|
|
} from '@nestjs/common';
|
|
import { DataSource } from 'typeorm';
|
|
import { administrationAuditContext } from '../administration/common/administration-audit';
|
|
import { AssetHistoryService } from '../asset-master/asset-history.service';
|
|
import { AuditService } from '../audit/audit.service';
|
|
import type { AuthPrincipal, RequestWithContext } from '../common/http/request-context';
|
|
import { AssetVersionChangeType, AuditAction } from '../database/entities';
|
|
import type { CreateFieldInventoryDto } from './dto/create-field-inventory.dto';
|
|
import { FieldInventoryService } from './field-inventory.service';
|
|
|
|
type FamilyRow = {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
level: 'INSTALLATION' | 'SUBINSTALLATION';
|
|
informationLabels: string[];
|
|
sourceReference: string | null;
|
|
isOther: boolean;
|
|
};
|
|
|
|
type ParentRow = {
|
|
id: string;
|
|
typeCode: string;
|
|
inventoryFamilyId: string | null;
|
|
};
|
|
|
|
type FieldTypesBase = {
|
|
context: {
|
|
area: { id: string; code: string; name: string };
|
|
operatorCompany: { id: string; code: string; name: string };
|
|
inspection: { id: string; code: string; status: string };
|
|
};
|
|
parent: { id: string; code: string; name: string };
|
|
data: Array<Record<string, unknown> & { id: string; code: string; name: string }>;
|
|
};
|
|
|
|
const STRUCTURAL_CHILD: Record<string, string | undefined> = {
|
|
area: 'yacimiento',
|
|
yacimiento: 'instalacion',
|
|
instalacion: 'subinstalacion',
|
|
};
|
|
|
|
const OTHER_FAMILY_SQL = "source_reference LIKE 'SYSTEM:F3.1:OTHER%' OR source_reference LIKE 'F5:SYSTEM:OTHER:%'";
|
|
|
|
@Injectable()
|
|
export class F3FieldInventoryStructureService {
|
|
constructor(
|
|
private readonly dataSource: DataSource,
|
|
private readonly fieldInventory: FieldInventoryService,
|
|
private readonly history: AssetHistoryService,
|
|
private readonly audit: AuditService,
|
|
) {}
|
|
|
|
async types(
|
|
visitId: string,
|
|
parentId: string | undefined,
|
|
principal: AuthPrincipal,
|
|
) {
|
|
const base = await this.fieldInventory.types(visitId, parentId, principal) as unknown as FieldTypesBase;
|
|
const effectiveParentId = parentId ?? base.parent.id;
|
|
const parent = await this.parent(effectiveParentId);
|
|
const expectedTypeCode = STRUCTURAL_CHILD[parent.typeCode.toLowerCase()];
|
|
const data = expectedTypeCode
|
|
? base.data.filter((type) => type.code.toLowerCase() === expectedTypeCode)
|
|
: [];
|
|
const families = await this.familiesFor(parent, expectedTypeCode);
|
|
|
|
return {
|
|
...base,
|
|
data: data.map((type) => ({
|
|
...type,
|
|
structuralKind: type.code.toUpperCase(),
|
|
families,
|
|
familyRequired: type.code.toLowerCase() === 'instalacion' || type.code.toLowerCase() === 'subinstalacion',
|
|
})),
|
|
};
|
|
}
|
|
|
|
async create(
|
|
visitId: string,
|
|
dto: CreateFieldInventoryDto,
|
|
principal: AuthPrincipal,
|
|
request: RequestWithContext,
|
|
) {
|
|
const options = await this.types(visitId, dto.parentId, principal);
|
|
const selectedType = options.data.find((type) => type.id === dto.typeId);
|
|
if (!selectedType) {
|
|
throw new BadRequestException({
|
|
code: 'FIELD_INVENTORY_STRUCTURE_TYPE_INVALID',
|
|
message: 'El tipo elegido no corresponde al siguiente nivel estructural permitido',
|
|
});
|
|
}
|
|
|
|
const typeCode = String(selectedType.code).toLowerCase();
|
|
const familyRequired = typeCode === 'instalacion' || typeCode === 'subinstalacion';
|
|
const families = (selectedType.families ?? []) as FamilyRow[];
|
|
if (familyRequired && !dto.familyId) {
|
|
throw new BadRequestException({
|
|
code: 'FIELD_INVENTORY_FAMILY_REQUIRED',
|
|
message: 'Elegí la familia técnica o la opción Otro / no catalogado',
|
|
});
|
|
}
|
|
if (!familyRequired && dto.familyId) {
|
|
throw new BadRequestException({
|
|
code: 'FIELD_INVENTORY_FAMILY_NOT_ALLOWED',
|
|
message: 'Este nivel estructural no utiliza familia técnica',
|
|
});
|
|
}
|
|
const family = dto.familyId
|
|
? families.find((item) => item.id === dto.familyId)
|
|
: null;
|
|
if (dto.familyId && !family) {
|
|
throw new BadRequestException({
|
|
code: 'FIELD_INVENTORY_FAMILY_INVALID',
|
|
message: 'La familia técnica no es válida para el padre seleccionado',
|
|
});
|
|
}
|
|
|
|
const created = await this.fieldInventory.create(visitId, dto, principal, request) as {
|
|
asset: { id: string; code: string; name: string };
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
// Área/Yacimiento remain structural context. A concrete Installation/Subinstallation
|
|
// created in field is a real Inventory instance from the moment it is registered.
|
|
await this.dataSource.query(`
|
|
UPDATE assets
|
|
SET is_inventory_instance=$3::boolean,updated_by=$2::uuid,updated_at=CURRENT_TIMESTAMP
|
|
WHERE id=$1::uuid
|
|
`, [
|
|
created.asset.id,
|
|
principal.userId,
|
|
typeCode === 'instalacion' || typeCode === 'subinstalacion',
|
|
]);
|
|
|
|
if (!family) return this.fieldInventory.detail(visitId, created.asset.id, principal);
|
|
|
|
await this.dataSource.transaction(async (manager) => {
|
|
const [current] = await manager.query(`
|
|
SELECT inventory_family_id AS "inventoryFamilyId"
|
|
FROM assets WHERE id=$1::uuid FOR UPDATE
|
|
`, [created.asset.id]) as Array<{ inventoryFamilyId: string | null }>;
|
|
if (!current) throw new ConflictException({
|
|
code: 'FIELD_INVENTORY_CREATED_ASSET_MISSING',
|
|
message: 'No se pudo clasificar el Inventario recién creado',
|
|
});
|
|
if (current.inventoryFamilyId === family.id) return;
|
|
|
|
await manager.query(`
|
|
UPDATE assets
|
|
SET inventory_family_id=$2::uuid,updated_by=$3::uuid,updated_at=CURRENT_TIMESTAMP
|
|
WHERE id=$1::uuid
|
|
`, [created.asset.id, family.id, principal.userId]);
|
|
const versionNumber = await this.history.capture(
|
|
manager,
|
|
created.asset.id,
|
|
AssetVersionChangeType.UPDATED,
|
|
principal,
|
|
request,
|
|
);
|
|
await this.audit.record({
|
|
...administrationAuditContext(principal, request),
|
|
action: AuditAction.ASSET_UPDATED,
|
|
entityType: 'asset',
|
|
entityId: created.asset.id,
|
|
beforeData: { inventoryFamilyId: current.inventoryFamilyId },
|
|
afterData: {
|
|
inventoryFamilyId: family.id,
|
|
inventoryFamilyCode: family.code,
|
|
inventoryFamilyName: family.name,
|
|
},
|
|
metadata: {
|
|
operation: 'FIELD_INVENTORY_FAMILY_ASSIGNED',
|
|
visitId,
|
|
versionNumber,
|
|
isOtherFamily: family.isOther,
|
|
},
|
|
}, manager);
|
|
});
|
|
|
|
return this.fieldInventory.detail(visitId, created.asset.id, principal);
|
|
}
|
|
|
|
private async parent(parentId: string): Promise<ParentRow> {
|
|
const rows = await this.dataSource.query(`
|
|
SELECT asset.id,type.code AS "typeCode",asset.inventory_family_id AS "inventoryFamilyId"
|
|
FROM assets asset
|
|
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
WHERE asset.id=$1::uuid AND asset.information_status<>'INACTIVE'
|
|
`, [parentId]) as ParentRow[];
|
|
if (!rows[0]) throw new BadRequestException({
|
|
code: 'FIELD_INVENTORY_PARENT_NOT_AVAILABLE',
|
|
message: 'El padre estructural elegido no está disponible',
|
|
});
|
|
return rows[0];
|
|
}
|
|
|
|
private async familiesFor(parent: ParentRow, expectedTypeCode: string | undefined): Promise<FamilyRow[]> {
|
|
if (expectedTypeCode === 'instalacion') {
|
|
return this.dataSource.query(`
|
|
SELECT id,code,name,level,information_labels AS "informationLabels",
|
|
source_reference AS "sourceReference",
|
|
(${OTHER_FAMILY_SQL}) AS "isOther"
|
|
FROM inventory_families
|
|
WHERE level='INSTALLATION' AND is_active=true
|
|
ORDER BY (${OTHER_FAMILY_SQL}) ASC,name,code
|
|
`) as Promise<FamilyRow[]>;
|
|
}
|
|
if (expectedTypeCode === 'subinstalacion') {
|
|
if (!parent.inventoryFamilyId) {
|
|
throw new ConflictException({
|
|
code: 'FIELD_INVENTORY_PARENT_FAMILY_REQUIRED',
|
|
message: 'La Instalación debe tener una familia técnica antes de agregar Subinstalaciones',
|
|
});
|
|
}
|
|
return this.dataSource.query(`
|
|
SELECT family.id,family.code,family.name,family.level,
|
|
family.information_labels AS "informationLabels",
|
|
family.source_reference AS "sourceReference",
|
|
(family.source_reference LIKE 'SYSTEM:F3.1:OTHER%' OR family.source_reference LIKE 'F5:SYSTEM:OTHER:%') AS "isOther"
|
|
FROM inventory_family_parent_rules rule
|
|
JOIN inventory_families family ON family.id=rule.child_family_id
|
|
WHERE rule.parent_family_id=$1::uuid
|
|
AND family.level='SUBINSTALLATION'
|
|
AND family.is_active=true
|
|
ORDER BY (family.source_reference LIKE 'SYSTEM:F3.1:OTHER%' OR family.source_reference LIKE 'F5:SYSTEM:OTHER:%') ASC,family.name,family.code
|
|
`, [parent.inventoryFamilyId]) as Promise<FamilyRow[]>;
|
|
}
|
|
return [];
|
|
}
|
|
}
|