feat(f6): administrar compatibilidades y campos técnicos por familia
This commit is contained in:
@@ -15,7 +15,13 @@ import type {
|
|||||||
ReplaceInventoryFamilyFindingsDto,
|
ReplaceInventoryFamilyFindingsDto,
|
||||||
UpdateInventoryFamilyDto,
|
UpdateInventoryFamilyDto,
|
||||||
} from './dto/inventory-family-admin.dto';
|
} from './dto/inventory-family-admin.dto';
|
||||||
|
import type {
|
||||||
|
CreateInventoryFamilyAttributeDto,
|
||||||
|
InventoryFamilyAttributeDataType,
|
||||||
|
UpdateInventoryFamilyAttributeDto,
|
||||||
|
} from './dto/inventory-family-attribute.dto';
|
||||||
|
|
||||||
|
type FamilyParent = { id: string; code: string; name: string };
|
||||||
type FamilyRow = {
|
type FamilyRow = {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -24,12 +30,24 @@ type FamilyRow = {
|
|||||||
informationLabels: string[];
|
informationLabels: string[];
|
||||||
sourceReference: string | null;
|
sourceReference: string | null;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
parentFamilyId: string | null;
|
parentFamilyIds: string[];
|
||||||
parentFamilyCode: string | null;
|
parentFamilies: FamilyParent[];
|
||||||
parentFamilyName: string | null;
|
|
||||||
assetCount: number;
|
assetCount: number;
|
||||||
findingCount: number;
|
findingCount: number;
|
||||||
findingItemIds: string[];
|
findingItemIds: string[];
|
||||||
|
technicalAttributeCount: number;
|
||||||
|
};
|
||||||
|
type AttributeRow = {
|
||||||
|
id: string;
|
||||||
|
inventoryFamilyId: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
dataType: InventoryFamilyAttributeDataType;
|
||||||
|
isRequired: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
unit: string | null;
|
||||||
|
options: string[] | null;
|
||||||
|
sortOrder: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -40,27 +58,7 @@ export class InventoryFamilyCatalogService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async listAdmin() {
|
async listAdmin() {
|
||||||
const data = await this.dataSource.query(`
|
const data = await this.dataSource.query(this.familySelect(`ORDER BY family.level,family.is_active DESC,family.name,family.code`)) as FamilyRow[];
|
||||||
SELECT
|
|
||||||
family.id,family.code,family.name,family.level,
|
|
||||||
family.information_labels AS "informationLabels",
|
|
||||||
family.source_reference AS "sourceReference",
|
|
||||||
family.is_active AS "isActive",
|
|
||||||
parent.id AS "parentFamilyId",parent.code AS "parentFamilyCode",parent.name AS "parentFamilyName",
|
|
||||||
(SELECT COUNT(*)::integer FROM assets asset WHERE asset.inventory_family_id=family.id) AS "assetCount",
|
|
||||||
(SELECT COUNT(*)::integer FROM finding_catalog_item_inventory_families mapping WHERE mapping.inventory_family_id=family.id) AS "findingCount",
|
|
||||||
COALESCE((
|
|
||||||
SELECT JSONB_AGG(mapping.catalog_item_id ORDER BY item.title,item.id)
|
|
||||||
FROM finding_catalog_item_inventory_families mapping
|
|
||||||
JOIN finding_catalog_items item ON item.id=mapping.catalog_item_id
|
|
||||||
WHERE mapping.inventory_family_id=family.id
|
|
||||||
),'[]'::jsonb) AS "findingItemIds"
|
|
||||||
FROM inventory_families family
|
|
||||||
LEFT JOIN inventory_family_parent_rules rule ON rule.child_family_id=family.id
|
|
||||||
LEFT JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
|
||||||
ORDER BY family.level,family.is_active DESC,
|
|
||||||
COALESCE(parent.name,''),family.name,family.code
|
|
||||||
`) as FamilyRow[];
|
|
||||||
return { data };
|
return { data };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,27 +78,28 @@ export class InventoryFamilyCatalogService {
|
|||||||
return { family, items, count: items.length };
|
return { family, items, count: items.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async attributes(familyId: string) {
|
||||||
|
const family = await this.family(familyId, false);
|
||||||
|
const items = await this.attributeRows(this.dataSource.manager, familyId);
|
||||||
|
return { family, items, count: items.length };
|
||||||
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
dto: CreateInventoryFamilyDto,
|
dto: CreateInventoryFamilyDto,
|
||||||
principal: AuthPrincipal,
|
principal: AuthPrincipal,
|
||||||
request: RequestWithContext,
|
request: RequestWithContext,
|
||||||
) {
|
) {
|
||||||
return this.dataSource.transaction(async (manager) => {
|
return this.dataSource.transaction(async (manager) => {
|
||||||
const parentId = await this.validateParent(manager,dto.level,dto.parentFamilyId ?? null,null);
|
const parentIds = await this.validateParents(manager, dto.level, dto.parentFamilyIds ?? [], null);
|
||||||
const code = `CUSTOM-${dto.level === 'INSTALLATION' ? 'I' : 'S'}-${randomUUID().slice(0,8).toUpperCase()}`;
|
const code = `CUSTOM-${dto.level === 'INSTALLATION' ? 'I' : 'S'}-${randomUUID().slice(0,8).toUpperCase()}`;
|
||||||
const [inserted] = (await manager.query(`
|
const [inserted] = (await manager.query(`
|
||||||
INSERT INTO inventory_families(
|
INSERT INTO inventory_families(
|
||||||
code,name,level,legacy_type_code,information_labels,source_reference,is_active
|
code,name,level,legacy_type_code,information_labels,source_reference,is_active
|
||||||
) VALUES ($1,$2,$3,NULL,$4::jsonb,'MANUAL:F5',true)
|
) VALUES ($1,$2,$3,NULL,$4::jsonb,'MANUAL:F6',true)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,[code,dto.name,dto.level,JSON.stringify(this.cleanLabels(dto.informationLabels ?? []))])) as Array<{id:string}>;
|
`,[code,dto.name,dto.level,JSON.stringify(this.cleanLabels(dto.informationLabels ?? []))])) as Array<{id:string}>;
|
||||||
if (!inserted) throw new Error('No se pudo crear la clasificación de Inventario');
|
if (!inserted) throw new Error('No se pudo crear la clasificación de Inventario');
|
||||||
if (parentId) {
|
await this.replaceParents(manager, inserted.id, parentIds);
|
||||||
await manager.query(`
|
|
||||||
INSERT INTO inventory_family_parent_rules(child_family_id,parent_family_id)
|
|
||||||
VALUES ($1::uuid,$2::uuid)
|
|
||||||
`,[inserted.id,parentId]);
|
|
||||||
}
|
|
||||||
const created = await this.family(inserted.id,false,manager);
|
const created = await this.family(inserted.id,false,manager);
|
||||||
await this.audit.record({
|
await this.audit.record({
|
||||||
...administrationAuditContext(principal,request),
|
...administrationAuditContext(principal,request),
|
||||||
@@ -108,7 +107,7 @@ export class InventoryFamilyCatalogService {
|
|||||||
entityType: 'inventory_family',
|
entityType: 'inventory_family',
|
||||||
entityId: inserted.id,
|
entityId: inserted.id,
|
||||||
afterData: created as unknown as Record<string,unknown>,
|
afterData: created as unknown as Record<string,unknown>,
|
||||||
metadata: { operation:'INVENTORY_FAMILY_CREATED', source:'MANUAL:F5' },
|
metadata: { operation:'INVENTORY_FAMILY_CREATED', source:'MANUAL:F6' },
|
||||||
},manager);
|
},manager);
|
||||||
return created;
|
return created;
|
||||||
});
|
});
|
||||||
@@ -122,10 +121,13 @@ export class InventoryFamilyCatalogService {
|
|||||||
) {
|
) {
|
||||||
return this.dataSource.transaction(async (manager) => {
|
return this.dataSource.transaction(async (manager) => {
|
||||||
const before = await this.family(familyId,false,manager,true);
|
const before = await this.family(familyId,false,manager,true);
|
||||||
const nextParentId = dto.parentFamilyId === undefined
|
const nextParentIds = dto.parentFamilyIds === undefined
|
||||||
? before.parentFamilyId
|
? before.parentFamilyIds
|
||||||
: dto.parentFamilyId;
|
: dto.parentFamilyIds;
|
||||||
const parentId = await this.validateParent(manager,before.level,nextParentId ?? null,familyId);
|
const parentIds = await this.validateParents(manager,before.level,nextParentIds,familyId);
|
||||||
|
if (before.level === 'SUBINSTALLATION' && dto.parentFamilyIds !== undefined) {
|
||||||
|
await this.assertRemovedCompatibilitiesUnused(manager, familyId, parentIds);
|
||||||
|
}
|
||||||
await manager.query(`
|
await manager.query(`
|
||||||
UPDATE inventory_families SET
|
UPDATE inventory_families SET
|
||||||
name=COALESCE($2::varchar,name),
|
name=COALESCE($2::varchar,name),
|
||||||
@@ -139,15 +141,7 @@ export class InventoryFamilyCatalogService {
|
|||||||
dto.informationLabels === undefined ? null : JSON.stringify(this.cleanLabels(dto.informationLabels)),
|
dto.informationLabels === undefined ? null : JSON.stringify(this.cleanLabels(dto.informationLabels)),
|
||||||
dto.isActive ?? null,
|
dto.isActive ?? null,
|
||||||
]);
|
]);
|
||||||
if (before.level==='SUBINSTALLATION') {
|
if (dto.parentFamilyIds !== undefined) await this.replaceParents(manager, familyId, parentIds);
|
||||||
await manager.query(`DELETE FROM inventory_family_parent_rules WHERE child_family_id=$1::uuid`,[familyId]);
|
|
||||||
if (parentId) {
|
|
||||||
await manager.query(`
|
|
||||||
INSERT INTO inventory_family_parent_rules(child_family_id,parent_family_id)
|
|
||||||
VALUES ($1::uuid,$2::uuid)
|
|
||||||
`,[familyId,parentId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const after = await this.family(familyId,false,manager);
|
const after = await this.family(familyId,false,manager);
|
||||||
await this.audit.record({
|
await this.audit.record({
|
||||||
...administrationAuditContext(principal,request),
|
...administrationAuditContext(principal,request),
|
||||||
@@ -162,6 +156,85 @@ export class InventoryFamilyCatalogService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createAttribute(
|
||||||
|
familyId: string,
|
||||||
|
dto: CreateInventoryFamilyAttributeDto,
|
||||||
|
principal: AuthPrincipal,
|
||||||
|
request: RequestWithContext,
|
||||||
|
) {
|
||||||
|
return this.dataSource.transaction(async (manager) => {
|
||||||
|
await this.family(familyId,false,manager,true);
|
||||||
|
const options = this.attributeOptions(dto.dataType,dto.options);
|
||||||
|
try {
|
||||||
|
const [created] = (await manager.query(`
|
||||||
|
INSERT INTO inventory_family_attribute_definitions(
|
||||||
|
inventory_family_id,code,name,data_type,is_required,is_active,unit,options,sort_order,created_by,updated_by
|
||||||
|
) VALUES ($1::uuid,$2,$3,$4::asset_attribute_data_type,$5,true,$6,$7::jsonb,$8,$9::uuid,$9::uuid)
|
||||||
|
RETURNING id,inventory_family_id AS "inventoryFamilyId",code,name,data_type AS "dataType",
|
||||||
|
is_required AS "isRequired",is_active AS "isActive",unit,options,sort_order AS "sortOrder"
|
||||||
|
`,[familyId,dto.code,dto.name,dto.dataType,dto.isRequired ?? false,dto.unit ?? null,
|
||||||
|
options === null ? null : JSON.stringify(options),dto.sortOrder ?? 0,principal.userId])) as AttributeRow[];
|
||||||
|
if (!created) throw new Error('No se pudo crear el campo técnico');
|
||||||
|
await this.audit.record({
|
||||||
|
...administrationAuditContext(principal,request),
|
||||||
|
action: AuditAction.ASSET_UPDATED,
|
||||||
|
entityType:'inventory_family_attribute',entityId:created.id,
|
||||||
|
afterData:created as unknown as Record<string,unknown>,
|
||||||
|
metadata:{operation:'INVENTORY_FAMILY_ATTRIBUTE_CREATED',inventoryFamilyId:familyId},
|
||||||
|
},manager);
|
||||||
|
return created;
|
||||||
|
} catch (error) {
|
||||||
|
if (this.isUniqueViolation(error)) throw new ConflictException({
|
||||||
|
code:'INVENTORY_FAMILY_ATTRIBUTE_CODE_EXISTS',
|
||||||
|
message:'Ya existe un campo técnico con ese código en la clasificación',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAttribute(
|
||||||
|
familyId: string,
|
||||||
|
attributeId: string,
|
||||||
|
dto: UpdateInventoryFamilyAttributeDto,
|
||||||
|
principal: AuthPrincipal,
|
||||||
|
request: RequestWithContext,
|
||||||
|
) {
|
||||||
|
return this.dataSource.transaction(async (manager) => {
|
||||||
|
await this.family(familyId,false,manager,true);
|
||||||
|
const before = await this.attribute(manager,familyId,attributeId,true);
|
||||||
|
const nextType = dto.dataType ?? before.dataType;
|
||||||
|
const nextOptions = dto.options === undefined
|
||||||
|
? this.attributeOptions(nextType,before.options ?? undefined)
|
||||||
|
: this.attributeOptions(nextType,dto.options ?? undefined);
|
||||||
|
const [after] = (await manager.query(`
|
||||||
|
UPDATE inventory_family_attribute_definitions SET
|
||||||
|
name=COALESCE($3::varchar,name),
|
||||||
|
data_type=COALESCE($4::asset_attribute_data_type,data_type),
|
||||||
|
is_required=COALESCE($5::boolean,is_required),
|
||||||
|
is_active=COALESCE($6::boolean,is_active),
|
||||||
|
unit=CASE WHEN $7::boolean THEN $8::varchar ELSE unit END,
|
||||||
|
options=$9::jsonb,
|
||||||
|
sort_order=COALESCE($10::integer,sort_order),
|
||||||
|
updated_by=$11::uuid,updated_at=CURRENT_TIMESTAMP
|
||||||
|
WHERE id=$1::uuid AND inventory_family_id=$2::uuid
|
||||||
|
RETURNING id,inventory_family_id AS "inventoryFamilyId",code,name,data_type AS "dataType",
|
||||||
|
is_required AS "isRequired",is_active AS "isActive",unit,options,sort_order AS "sortOrder"
|
||||||
|
`,[attributeId,familyId,dto.name ?? null,dto.dataType ?? null,dto.isRequired ?? null,dto.isActive ?? null,
|
||||||
|
dto.unit !== undefined,dto.unit ?? null,nextOptions === null ? null : JSON.stringify(nextOptions),
|
||||||
|
dto.sortOrder ?? null,principal.userId])) as AttributeRow[];
|
||||||
|
if (!after) throw new NotFoundException({code:'INVENTORY_FAMILY_ATTRIBUTE_NOT_FOUND',message:'El campo técnico no existe'});
|
||||||
|
await this.audit.record({
|
||||||
|
...administrationAuditContext(principal,request),
|
||||||
|
action: AuditAction.ASSET_UPDATED,
|
||||||
|
entityType:'inventory_family_attribute',entityId:attributeId,
|
||||||
|
beforeData:before as unknown as Record<string,unknown>,afterData:after as unknown as Record<string,unknown>,
|
||||||
|
metadata:{operation:'INVENTORY_FAMILY_ATTRIBUTE_UPDATED',inventoryFamilyId:familyId},
|
||||||
|
},manager);
|
||||||
|
return after;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async replaceFindings(
|
async replaceFindings(
|
||||||
familyId: string,
|
familyId: string,
|
||||||
dto: ReplaceInventoryFamilyFindingsDto,
|
dto: ReplaceInventoryFamilyFindingsDto,
|
||||||
@@ -178,13 +251,11 @@ export class InventoryFamilyCatalogService {
|
|||||||
JOIN finding_categories category ON category.id=item.category_id
|
JOIN finding_categories category ON category.id=item.category_id
|
||||||
WHERE item.id=ANY($1::uuid[]) AND item.is_active=true AND category.is_active=true
|
WHERE item.id=ANY($1::uuid[]) AND item.is_active=true AND category.is_active=true
|
||||||
`,[uniqueIds])) as Array<{total:number}>;
|
`,[uniqueIds])) as Array<{total:number}>;
|
||||||
if (Number(count?.total ?? 0)!==uniqueIds.length) {
|
if (Number(count?.total ?? 0)!==uniqueIds.length) throw new BadRequestException({
|
||||||
throw new BadRequestException({
|
|
||||||
code:'INVENTORY_FAMILY_FINDING_INVALID',
|
code:'INVENTORY_FAMILY_FINDING_INVALID',
|
||||||
message:'Uno o más Hallazgos elegidos no están activos en el catálogo',
|
message:'Uno o más Hallazgos elegidos no están activos en el catálogo',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const beforeIds=family.findingItemIds;
|
const beforeIds=family.findingItemIds;
|
||||||
await manager.query(`DELETE FROM finding_catalog_item_inventory_families WHERE inventory_family_id=$1::uuid`,[familyId]);
|
await manager.query(`DELETE FROM finding_catalog_item_inventory_families WHERE inventory_family_id=$1::uuid`,[familyId]);
|
||||||
if (uniqueIds.length) {
|
if (uniqueIds.length) {
|
||||||
@@ -198,11 +269,9 @@ export class InventoryFamilyCatalogService {
|
|||||||
await this.audit.record({
|
await this.audit.record({
|
||||||
...administrationAuditContext(principal,request),
|
...administrationAuditContext(principal,request),
|
||||||
action: AuditAction.ASSET_UPDATED,
|
action: AuditAction.ASSET_UPDATED,
|
||||||
entityType:'inventory_family_findings',
|
entityType:'inventory_family_findings',entityId:familyId,
|
||||||
entityId:familyId,
|
beforeData:{itemIds:beforeIds},afterData:{itemIds:after.findingItemIds},
|
||||||
beforeData:{ itemIds:beforeIds },
|
metadata:{operation:'INVENTORY_FAMILY_FINDINGS_REPLACED',reason:dto.reason},
|
||||||
afterData:{ itemIds:after.findingItemIds },
|
|
||||||
metadata:{ operation:'INVENTORY_FAMILY_FINDINGS_REPLACED', reason:dto.reason },
|
|
||||||
},manager);
|
},manager);
|
||||||
return this.findingsWithManager(manager,familyId);
|
return this.findingsWithManager(manager,familyId);
|
||||||
});
|
});
|
||||||
@@ -217,77 +286,150 @@ export class InventoryFamilyCatalogService {
|
|||||||
FROM finding_catalog_item_inventory_families mapping
|
FROM finding_catalog_item_inventory_families mapping
|
||||||
JOIN finding_catalog_items item ON item.id=mapping.catalog_item_id
|
JOIN finding_catalog_items item ON item.id=mapping.catalog_item_id
|
||||||
JOIN finding_categories category ON category.id=item.category_id
|
JOIN finding_categories category ON category.id=item.category_id
|
||||||
WHERE mapping.inventory_family_id=$1::uuid
|
WHERE mapping.inventory_family_id=$1::uuid AND item.is_active=true AND category.is_active=true
|
||||||
AND item.is_active=true AND category.is_active=true
|
|
||||||
ORDER BY category.sort_order,item.source_number,item.title
|
ORDER BY category.sort_order,item.source_number,item.title
|
||||||
`,[familyId]);
|
`,[familyId]);
|
||||||
return {family,items,count:items.length};
|
return {family,items,count:items.length};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private familySelect(suffix:string) {
|
||||||
|
return `
|
||||||
|
SELECT family.id,family.code,family.name,family.level,
|
||||||
|
family.information_labels AS "informationLabels",
|
||||||
|
family.source_reference AS "sourceReference",family.is_active AS "isActive",
|
||||||
|
COALESCE((SELECT JSONB_AGG(rule.parent_family_id ORDER BY parent.name,parent.code)
|
||||||
|
FROM inventory_family_parent_rules rule
|
||||||
|
JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
||||||
|
WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilyIds",
|
||||||
|
COALESCE((SELECT JSONB_AGG(JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) ORDER BY parent.name,parent.code)
|
||||||
|
FROM inventory_family_parent_rules rule
|
||||||
|
JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
||||||
|
WHERE rule.child_family_id=family.id),'[]'::jsonb) AS "parentFamilies",
|
||||||
|
(SELECT COUNT(*)::integer FROM assets asset WHERE asset.inventory_family_id=family.id) AS "assetCount",
|
||||||
|
(SELECT COUNT(*)::integer FROM finding_catalog_item_inventory_families mapping WHERE mapping.inventory_family_id=family.id) AS "findingCount",
|
||||||
|
COALESCE((SELECT JSONB_AGG(mapping.catalog_item_id ORDER BY mapping.catalog_item_id)
|
||||||
|
FROM finding_catalog_item_inventory_families mapping WHERE mapping.inventory_family_id=family.id),'[]'::jsonb) AS "findingItemIds",
|
||||||
|
(SELECT COUNT(*)::integer FROM inventory_family_attribute_definitions definition
|
||||||
|
WHERE definition.inventory_family_id=family.id AND definition.is_active=true) AS "technicalAttributeCount"
|
||||||
|
FROM inventory_families family
|
||||||
|
${suffix}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private async family(
|
private async family(
|
||||||
familyId:string,
|
familyId:string,
|
||||||
activeOnly:boolean,
|
activeOnly:boolean,
|
||||||
manager:EntityManager=this.dataSource.manager,
|
manager:EntityManager=this.dataSource.manager,
|
||||||
lock=false,
|
lock=false,
|
||||||
):Promise<FamilyRow> {
|
):Promise<FamilyRow> {
|
||||||
const rows=(await manager.query(`
|
const lockClause=lock ? 'FOR UPDATE OF family' : '';
|
||||||
SELECT family.id,family.code,family.name,family.level,
|
const rows=(await manager.query(this.familySelect(`
|
||||||
family.information_labels AS "informationLabels",
|
|
||||||
family.source_reference AS "sourceReference",family.is_active AS "isActive",
|
|
||||||
parent.id AS "parentFamilyId",parent.code AS "parentFamilyCode",parent.name AS "parentFamilyName",
|
|
||||||
(SELECT COUNT(*)::integer FROM assets asset WHERE asset.inventory_family_id=family.id) AS "assetCount",
|
|
||||||
(SELECT COUNT(*)::integer FROM finding_catalog_item_inventory_families mapping WHERE mapping.inventory_family_id=family.id) AS "findingCount",
|
|
||||||
COALESCE((SELECT JSONB_AGG(mapping.catalog_item_id ORDER BY mapping.catalog_item_id)
|
|
||||||
FROM finding_catalog_item_inventory_families mapping WHERE mapping.inventory_family_id=family.id),'[]'::jsonb) AS "findingItemIds"
|
|
||||||
FROM inventory_families family
|
|
||||||
LEFT JOIN inventory_family_parent_rules rule ON rule.child_family_id=family.id
|
|
||||||
LEFT JOIN inventory_families parent ON parent.id=rule.parent_family_id
|
|
||||||
WHERE family.id=$1::uuid ${activeOnly ? 'AND family.is_active=true' : ''}
|
WHERE family.id=$1::uuid ${activeOnly ? 'AND family.is_active=true' : ''}
|
||||||
${lock ? 'FOR UPDATE OF family' : ''}
|
${lockClause}
|
||||||
`,[familyId])) as FamilyRow[];
|
`),[familyId])) as FamilyRow[];
|
||||||
if (!rows[0]) throw new NotFoundException({
|
if (!rows[0]) throw new NotFoundException({
|
||||||
code:'INVENTORY_FAMILY_NOT_FOUND',message:'La clasificación de Inventario no existe',
|
code:'INVENTORY_FAMILY_NOT_FOUND',message:'La clasificación de Inventario no existe',
|
||||||
});
|
});
|
||||||
return rows[0];
|
return rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateParent(
|
private async validateParents(
|
||||||
manager:EntityManager,
|
manager:EntityManager,
|
||||||
level:'INSTALLATION'|'SUBINSTALLATION',
|
level:'INSTALLATION'|'SUBINSTALLATION',
|
||||||
parentFamilyId:string|null,
|
requestedIds:string[],
|
||||||
ownId:string|null,
|
ownId:string|null,
|
||||||
):Promise<string|null> {
|
):Promise<string[]> {
|
||||||
|
const ids=[...new Set(requestedIds)];
|
||||||
if (level==='INSTALLATION') {
|
if (level==='INSTALLATION') {
|
||||||
if (parentFamilyId) throw new BadRequestException({
|
if (ids.length) throw new BadRequestException({
|
||||||
code:'INVENTORY_FAMILY_PARENT_NOT_ALLOWED',
|
code:'INVENTORY_FAMILY_PARENT_NOT_ALLOWED',
|
||||||
message:'Una clasificación de Instalación no tiene clasificación padre',
|
message:'Una clasificación de Instalación no lleva compatibilidades padre',
|
||||||
});
|
});
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
if (!parentFamilyId) throw new BadRequestException({
|
if (!ids.length) throw new BadRequestException({
|
||||||
code:'INVENTORY_FAMILY_PARENT_REQUIRED',
|
code:'INVENTORY_FAMILY_PARENT_REQUIRED',
|
||||||
message:'Una Subinstalación debe pertenecer a un tipo de Instalación',
|
message:'Elegí al menos un tipo de Instalación compatible con esta Subinstalación',
|
||||||
});
|
});
|
||||||
if (parentFamilyId===ownId) throw new BadRequestException({
|
if (ownId && ids.includes(ownId)) throw new BadRequestException({
|
||||||
code:'INVENTORY_FAMILY_PARENT_CYCLE',message:'Una clasificación no puede ser su propio padre',
|
code:'INVENTORY_FAMILY_PARENT_CYCLE',message:'Una clasificación no puede ser compatible consigo misma',
|
||||||
});
|
});
|
||||||
const rows=(await manager.query(`
|
const [count]=(await manager.query(`
|
||||||
SELECT id FROM inventory_families
|
SELECT COUNT(*)::integer AS total FROM inventory_families
|
||||||
WHERE id=$1::uuid AND level='INSTALLATION' AND is_active=true
|
WHERE id=ANY($1::uuid[]) AND level='INSTALLATION' AND is_active=true
|
||||||
LIMIT 1
|
`,[ids])) as Array<{total:number}>;
|
||||||
`,[parentFamilyId])) as Array<{id:string}>;
|
if (Number(count?.total ?? 0)!==ids.length) throw new BadRequestException({
|
||||||
if (!rows[0]) throw new BadRequestException({
|
|
||||||
code:'INVENTORY_FAMILY_PARENT_INVALID',
|
code:'INVENTORY_FAMILY_PARENT_INVALID',
|
||||||
message:'La Subinstalación debe vincularse a una clasificación de Instalación activa',
|
message:'Todas las compatibilidades deben ser clasificaciones de Instalación activas',
|
||||||
});
|
});
|
||||||
return parentFamilyId;
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async replaceParents(manager:EntityManager,familyId:string,parentIds:string[]) {
|
||||||
|
await manager.query(`DELETE FROM inventory_family_parent_rules WHERE child_family_id=$1::uuid`,[familyId]);
|
||||||
|
if (parentIds.length) await manager.query(`
|
||||||
|
INSERT INTO inventory_family_parent_rules(child_family_id,parent_family_id)
|
||||||
|
SELECT $1::uuid,parent_id FROM UNNEST($2::uuid[]) AS selected(parent_id)
|
||||||
|
ON CONFLICT (child_family_id,parent_family_id) DO NOTHING
|
||||||
|
`,[familyId,parentIds]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertRemovedCompatibilitiesUnused(manager:EntityManager,childFamilyId:string,nextParentIds:string[]) {
|
||||||
|
const rows=await manager.query(`
|
||||||
|
SELECT child.name AS "childName",parent.name AS "parentName",COUNT(*)::integer AS total
|
||||||
|
FROM assets child
|
||||||
|
JOIN assets parent ON parent.id=child.parent_id
|
||||||
|
WHERE child.inventory_family_id=$1::uuid
|
||||||
|
AND parent.inventory_family_id IS NOT NULL
|
||||||
|
AND NOT (parent.inventory_family_id=ANY($2::uuid[]))
|
||||||
|
GROUP BY child.name,parent.name
|
||||||
|
ORDER BY total DESC
|
||||||
|
LIMIT 1
|
||||||
|
`,[childFamilyId,nextParentIds]);
|
||||||
|
if (rows[0]) throw new ConflictException({
|
||||||
|
code:'INVENTORY_FAMILY_COMPATIBILITY_IN_USE',
|
||||||
|
message:`No podés quitar esa compatibilidad: ya existen Subinstalaciones de este tipo dentro de ${rows[0].parentName}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async attributeRows(manager:EntityManager,familyId:string):Promise<AttributeRow[]> {
|
||||||
|
return manager.query(`
|
||||||
|
SELECT id,inventory_family_id AS "inventoryFamilyId",code,name,data_type AS "dataType",
|
||||||
|
is_required AS "isRequired",is_active AS "isActive",unit,options,sort_order AS "sortOrder"
|
||||||
|
FROM inventory_family_attribute_definitions
|
||||||
|
WHERE inventory_family_id=$1::uuid
|
||||||
|
ORDER BY is_active DESC,sort_order,name,code
|
||||||
|
`,[familyId]) as Promise<AttributeRow[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async attribute(manager:EntityManager,familyId:string,attributeId:string,lock=false):Promise<AttributeRow> {
|
||||||
|
const rows=(await manager.query(`
|
||||||
|
SELECT id,inventory_family_id AS "inventoryFamilyId",code,name,data_type AS "dataType",
|
||||||
|
is_required AS "isRequired",is_active AS "isActive",unit,options,sort_order AS "sortOrder"
|
||||||
|
FROM inventory_family_attribute_definitions
|
||||||
|
WHERE id=$1::uuid AND inventory_family_id=$2::uuid
|
||||||
|
${lock ? 'FOR UPDATE' : ''}
|
||||||
|
`,[attributeId,familyId])) as AttributeRow[];
|
||||||
|
if (!rows[0]) throw new NotFoundException({code:'INVENTORY_FAMILY_ATTRIBUTE_NOT_FOUND',message:'El campo técnico no existe'});
|
||||||
|
return rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private attributeOptions(type:InventoryFamilyAttributeDataType,raw:string[]|undefined):string[]|null {
|
||||||
|
if (type!=='SELECT') return null;
|
||||||
|
const values=[...new Map((raw ?? []).map((value) => {
|
||||||
|
const clean=value.trim(); return [clean.toLocaleLowerCase('es-AR'),clean] as const;
|
||||||
|
}).filter(([,value]) => Boolean(value))).values()];
|
||||||
|
if (!values.length) throw new BadRequestException({
|
||||||
|
code:'INVENTORY_FAMILY_ATTRIBUTE_OPTIONS_REQUIRED',
|
||||||
|
message:'Un campo de lista necesita al menos una opción',
|
||||||
|
});
|
||||||
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanLabels(labels:string[]):string[] {
|
private cleanLabels(labels:string[]):string[] {
|
||||||
const unique=new Map<string,string>();
|
const unique=new Map<string,string>();
|
||||||
for (const raw of labels) {
|
for (const raw of labels) {
|
||||||
const clean=raw.trim();
|
const clean=raw.trim(); if (!clean) continue;
|
||||||
if (!clean) continue;
|
|
||||||
const identity=clean.toLocaleLowerCase('es-AR');
|
const identity=clean.toLocaleLowerCase('es-AR');
|
||||||
if (!unique.has(identity)) unique.set(identity,clean);
|
if (!unique.has(identity)) unique.set(identity,clean);
|
||||||
}
|
}
|
||||||
@@ -296,4 +438,8 @@ export class InventoryFamilyCatalogService {
|
|||||||
});
|
});
|
||||||
return [...unique.values()];
|
return [...unique.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isUniqueViolation(error:unknown) {
|
||||||
|
return Boolean(error && typeof error==='object' && 'code' in error && (error as {code?:string}).code==='23505');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user