Reset controlado de datos operativos e históricos, nueva jerarquía manual Departamento → Área → Yacimiento → Instalación → Subinstalación, filtros y administración inline de Hallazgos, y corrección de alta con Área sin Operadora.
336 lines
15 KiB
TypeScript
336 lines
15 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { DataSource } from 'typeorm';
|
|
import type { InventoryBrowserQueryDto } from './dto/inventory-browser-query.dto';
|
|
|
|
type ParentContext = {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
typeCode: string;
|
|
};
|
|
|
|
@Injectable()
|
|
export class InventoryBrowserService {
|
|
constructor(private readonly dataSource: DataSource) {}
|
|
|
|
async items(query: InventoryBrowserQueryDto) {
|
|
const params: unknown[] = [];
|
|
const conditions = [
|
|
"asset.information_status<>'INACTIVE'",
|
|
'type.is_active=true',
|
|
"(type.operational_role='COMPANY' OR lower(type.code) IN ('departamento','area','yacimiento','instalacion','subinstalacion'))",
|
|
];
|
|
const add = (value: unknown): string => {
|
|
params.push(value);
|
|
return `$${params.length}`;
|
|
};
|
|
|
|
if (query.search?.trim()) {
|
|
const p = add(`%${query.search.trim()}%`);
|
|
conditions.push(`(asset.code ILIKE ${p} OR asset.name ILIKE ${p} OR COALESCE(asset.common_name,'') ILIKE ${p})`);
|
|
}
|
|
if (query.typeId) conditions.push(`asset.asset_type_id=${add(query.typeId)}::uuid`);
|
|
if (query.status) conditions.push(`asset.information_status=${add(query.status)}::asset_information_status`);
|
|
if (query.operationalStatus) conditions.push(`asset.operational_status=${add(query.operationalStatus)}::asset_operational_status`);
|
|
if (query.needsValidation === true) conditions.push("asset.information_status NOT IN ('VALIDATED','INACTIVE')");
|
|
if (query.needsValidation === false) conditions.push("asset.information_status='VALIDATED'");
|
|
if (query.hasGeometry === true) conditions.push('EXISTS (SELECT 1 FROM asset_geometries geometry_filter WHERE geometry_filter.asset_id=asset.id)');
|
|
if (query.hasGeometry === false) conditions.push('NOT EXISTS (SELECT 1 FROM asset_geometries geometry_filter WHERE geometry_filter.asset_id=asset.id)');
|
|
if (query.operationalAreaId) {
|
|
const area = add(query.operationalAreaId);
|
|
conditions.push(`(asset.id=${area}::uuid OR asset.operational_area_id=${area}::uuid)`);
|
|
}
|
|
if (query.operatorCompanyId) {
|
|
const company = add(query.operatorCompanyId);
|
|
conditions.push(`(
|
|
asset.id=${company}::uuid
|
|
OR EXISTS (
|
|
SELECT 1 FROM area_company_relations relation
|
|
WHERE (relation.area_id=asset.operational_area_id OR relation.area_id=asset.id)
|
|
AND relation.company_id=${company}::uuid
|
|
AND relation.relation_role='OPERATOR'
|
|
AND relation.valid_until IS NULL
|
|
)
|
|
)`);
|
|
}
|
|
|
|
const where = conditions.join(' AND ');
|
|
const [countRow] = (await this.dataSource.query(`
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM assets asset
|
|
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
WHERE ${where}
|
|
`,params)) as Array<{total:number}>;
|
|
const total=Number(countRow?.total ?? 0);
|
|
const offset=(query.page-1)*query.pageSize;
|
|
params.push(query.pageSize);
|
|
const limit=`$${params.length}`;
|
|
params.push(offset);
|
|
const offsetParam=`$${params.length}`;
|
|
|
|
const data=await this.dataSource.query(`
|
|
SELECT
|
|
asset.id,asset.code,asset.name,asset.common_name AS "commonName",
|
|
JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type,
|
|
CASE WHEN parent.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT(
|
|
'id',parent.id,'code',parent.code,'name',parent.name
|
|
) END AS parent,
|
|
CASE WHEN area.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT(
|
|
'id',area.id,'code',area.code,'name',area.name
|
|
) END AS "operationalArea",
|
|
(
|
|
SELECT JSONB_BUILD_OBJECT('id',company.id,'code',company.code,'name',company.name)
|
|
FROM area_company_relations relation
|
|
JOIN assets company ON company.id=relation.company_id
|
|
WHERE relation.area_id=COALESCE(asset.operational_area_id,
|
|
CASE WHEN type.operational_role='AREA' THEN asset.id ELSE NULL END)
|
|
AND relation.relation_role='OPERATOR'
|
|
AND relation.valid_until IS NULL
|
|
ORDER BY relation.valid_from DESC,relation.created_at DESC
|
|
LIMIT 1
|
|
) AS "operatorCompany",
|
|
asset.information_status AS "informationStatus",
|
|
asset.operational_status AS "operationalStatus",
|
|
(SELECT COUNT(*)::integer FROM assets child
|
|
WHERE child.parent_id=asset.id AND child.information_status<>'INACTIVE') AS "childrenCount",
|
|
EXISTS (SELECT 1 FROM asset_geometries geometry WHERE geometry.asset_id=asset.id) AS "hasGeometry",
|
|
CASE WHEN geometry_type.type IS NULL THEN NULL ELSE geometry_type.type END AS "geometryType",
|
|
(SELECT COUNT(*)::integer FROM asset_media media WHERE media.asset_id=asset.id AND media.deleted_at IS NULL) AS "mediaCount",
|
|
asset.data_origin AS "dataOrigin",
|
|
(asset.provenance_verified_at IS NOT NULL) AS "provenanceVerified",
|
|
asset.current_version AS "currentVersion",
|
|
asset.updated_at AS "updatedAt"
|
|
FROM assets asset
|
|
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
LEFT JOIN assets parent ON parent.id=asset.parent_id
|
|
LEFT JOIN assets area ON area.id=asset.operational_area_id
|
|
LEFT JOIN LATERAL (
|
|
SELECT ST_GeometryType(geometry.geometry)::text AS type
|
|
FROM asset_geometries geometry
|
|
WHERE geometry.asset_id=asset.id
|
|
ORDER BY geometry.updated_at DESC
|
|
LIMIT 1
|
|
) geometry_type ON true
|
|
WHERE ${where}
|
|
ORDER BY CASE
|
|
WHEN lower(type.code)='departamento' THEN 0
|
|
WHEN type.operational_role='COMPANY' THEN 1
|
|
WHEN lower(type.code)='area' THEN 2
|
|
WHEN lower(type.code)='yacimiento' THEN 3
|
|
WHEN lower(type.code)='instalacion' THEN 4
|
|
WHEN lower(type.code)='subinstalacion' THEN 5 ELSE 9 END,
|
|
asset.name,asset.code
|
|
LIMIT ${limit} OFFSET ${offsetParam}
|
|
`,params);
|
|
|
|
return {
|
|
data,
|
|
meta:{
|
|
page:query.page,
|
|
pageSize:query.pageSize,
|
|
total,
|
|
totalPages:total===0 ? 0 : Math.ceil(total/query.pageSize),
|
|
},
|
|
};
|
|
}
|
|
|
|
async departments(query: InventoryBrowserQueryDto) {
|
|
const params: unknown[] = [];
|
|
const conditions = [
|
|
"lower(type.code)='departamento'",
|
|
"department.information_status<>'INACTIVE'",
|
|
'type.is_active=true',
|
|
];
|
|
const add = (value: unknown): string => {
|
|
params.push(value);
|
|
return `$${params.length}`;
|
|
};
|
|
if (query.search?.trim()) {
|
|
const p = add(`%${query.search.trim()}%`);
|
|
conditions.push(`(department.code ILIKE ${p} OR department.name ILIKE ${p} OR COALESCE(department.common_name,'') ILIKE ${p})`);
|
|
}
|
|
if (query.operatorCompanyId) {
|
|
const p = add(query.operatorCompanyId);
|
|
conditions.push(`EXISTS (
|
|
SELECT 1
|
|
FROM assets area
|
|
JOIN asset_types atype ON atype.id=area.asset_type_id
|
|
JOIN area_company_relations relation ON relation.area_id=area.id
|
|
WHERE area.parent_id=department.id
|
|
AND lower(atype.code)='area'
|
|
AND relation.company_id=${p}::uuid
|
|
AND relation.relation_role='OPERATOR'
|
|
AND relation.valid_until IS NULL
|
|
)`);
|
|
}
|
|
|
|
const data = await this.dataSource.query(`
|
|
SELECT
|
|
department.id,department.code,department.name,department.common_name AS "commonName",
|
|
JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type,
|
|
department.information_status AS "informationStatus",
|
|
department.operational_status AS "operationalStatus",
|
|
(SELECT COUNT(*)::integer FROM assets area
|
|
JOIN asset_types atype ON atype.id=area.asset_type_id
|
|
WHERE area.parent_id=department.id AND lower(atype.code)='area'
|
|
AND area.information_status<>'INACTIVE') AS "areaCount",
|
|
(SELECT COUNT(*)::integer FROM assets child
|
|
WHERE child.parent_id=department.id AND child.information_status<>'INACTIVE') AS "childrenCount"
|
|
FROM assets department
|
|
JOIN asset_types type ON type.id=department.asset_type_id
|
|
WHERE ${conditions.join(' AND ')}
|
|
ORDER BY department.name,department.code
|
|
`, params);
|
|
return { data, meta: { count: data.length } };
|
|
}
|
|
|
|
async companies(query: InventoryBrowserQueryDto) {
|
|
const params: unknown[] = [];
|
|
const conditions = [
|
|
"type.operational_role='COMPANY'",
|
|
"company.information_status<>'INACTIVE'",
|
|
'type.is_active=true',
|
|
];
|
|
if (query.search?.trim()) {
|
|
params.push(`%${query.search.trim()}%`);
|
|
conditions.push(`(company.code ILIKE $1 OR company.name ILIKE $1 OR COALESCE(company.common_name,'') ILIKE $1)`);
|
|
}
|
|
const data = await this.dataSource.query(`
|
|
SELECT company.id,company.code,company.name,company.common_name AS "commonName",
|
|
JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type,
|
|
company.information_status AS "informationStatus",
|
|
(SELECT COUNT(*)::integer FROM area_company_relations relation
|
|
WHERE relation.company_id=company.id AND relation.relation_role='OPERATOR'
|
|
AND relation.valid_until IS NULL) AS "areaCount"
|
|
FROM assets company
|
|
JOIN asset_types type ON type.id=company.asset_type_id
|
|
WHERE ${conditions.join(' AND ')}
|
|
ORDER BY company.name,company.code
|
|
`,params);
|
|
return { data,meta:{count:data.length} };
|
|
}
|
|
|
|
async areas(query: InventoryBrowserQueryDto) {
|
|
const params: unknown[] = [];
|
|
const conditions = [
|
|
"type.operational_role='AREA'",
|
|
"area.information_status<>'INACTIVE'",
|
|
'type.is_active=true',
|
|
];
|
|
const add = (value: unknown): string => { params.push(value); return `$${params.length}`; };
|
|
if (query.search?.trim()) {
|
|
const p = add(`%${query.search.trim()}%`);
|
|
conditions.push(`(area.code ILIKE ${p} OR area.name ILIKE ${p} OR COALESCE(area.common_name,'') ILIKE ${p})`);
|
|
}
|
|
if (query.operationalAreaId) conditions.push(`area.id=${add(query.operationalAreaId)}::uuid`);
|
|
if (query.operatorCompanyId) {
|
|
const p = add(query.operatorCompanyId);
|
|
conditions.push(`EXISTS (SELECT 1 FROM area_company_relations relation
|
|
WHERE relation.area_id=area.id AND relation.company_id=${p}::uuid
|
|
AND relation.relation_role='OPERATOR' AND relation.valid_until IS NULL)`);
|
|
}
|
|
const data = await this.dataSource.query(`
|
|
SELECT area.id,area.code,area.name,area.common_name AS "commonName",
|
|
JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type,
|
|
area.information_status AS "informationStatus",area.operational_status AS "operationalStatus",
|
|
(SELECT JSONB_BUILD_OBJECT('id',company.id,'code',company.code,'name',company.name)
|
|
FROM area_company_relations relation JOIN assets company ON company.id=relation.company_id
|
|
WHERE relation.area_id=area.id AND relation.relation_role='OPERATOR' AND relation.valid_until IS NULL
|
|
ORDER BY relation.valid_from DESC,relation.created_at DESC LIMIT 1) AS "currentOperator",
|
|
(SELECT COUNT(*)::integer FROM assets yacimiento JOIN asset_types ytype ON ytype.id=yacimiento.asset_type_id
|
|
WHERE yacimiento.parent_id=area.id AND lower(ytype.code)='yacimiento'
|
|
AND yacimiento.information_status<>'INACTIVE') AS "yacimientoCount",
|
|
(SELECT COUNT(*)::integer FROM assets inventory
|
|
WHERE inventory.information_status<>'INACTIVE' AND inventory.operational_area_id=area.id) AS "inventoryCount"
|
|
FROM assets area JOIN asset_types type ON type.id=area.asset_type_id
|
|
WHERE ${conditions.join(' AND ')}
|
|
ORDER BY area.name,area.code
|
|
`, params);
|
|
return { data, meta: { count: data.length } };
|
|
}
|
|
|
|
async children(parentId: string, query: InventoryBrowserQueryDto) {
|
|
const parent = await this.parent(parentId);
|
|
const allowedChildType = this.allowedChildType(parent.typeCode);
|
|
if (!allowedChildType) return { parent, data: [], meta: { count: 0, hasMore: false } };
|
|
|
|
const params: unknown[] = [parentId, allowedChildType];
|
|
const conditions = [
|
|
'asset.parent_id=$1::uuid',
|
|
'lower(type.code)=lower($2)',
|
|
"asset.information_status<>'INACTIVE'",
|
|
'type.is_active=true',
|
|
];
|
|
if (query.search?.trim()) {
|
|
params.push(`%${query.search.trim()}%`);
|
|
const p = `$${params.length}`;
|
|
conditions.push(`(asset.code ILIKE ${p} OR asset.name ILIKE ${p} OR COALESCE(asset.common_name,'') ILIKE ${p})`);
|
|
}
|
|
|
|
params.push(201);
|
|
const limit = `$${params.length}`;
|
|
const rows = await this.dataSource.query(`
|
|
SELECT
|
|
asset.id,asset.code,asset.name,asset.common_name AS "commonName",
|
|
JSONB_BUILD_OBJECT('id',type.id,'code',type.code,'name',type.name) AS type,
|
|
CASE WHEN parent_asset.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT(
|
|
'id',parent_asset.id,'code',parent_asset.code,'name',parent_asset.name
|
|
) END AS parent,
|
|
asset.information_status AS "informationStatus",
|
|
asset.operational_status AS "operationalStatus",
|
|
asset.is_inventory_instance AS "isInventoryInstance",
|
|
CASE WHEN family.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT(
|
|
'id',family.id,'code',family.code,'name',family.name,'level',family.level
|
|
) END AS "inventoryFamily",
|
|
(SELECT COUNT(*)::integer FROM assets child
|
|
WHERE child.parent_id=asset.id AND child.information_status<>'INACTIVE') AS "childrenCount",
|
|
CASE WHEN family.id IS NULL THEN 0 ELSE (
|
|
SELECT COUNT(*)::integer FROM finding_catalog_item_inventory_families mapping
|
|
WHERE mapping.inventory_family_id=family.id
|
|
) END AS "findingCount",
|
|
EXISTS (SELECT 1 FROM asset_geometries geometry WHERE geometry.asset_id=asset.id) AS "hasGeometry",
|
|
asset.updated_at AS "updatedAt"
|
|
FROM assets asset
|
|
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
LEFT JOIN assets parent_asset ON parent_asset.id=asset.parent_id
|
|
LEFT JOIN inventory_families family ON family.id=asset.inventory_family_id
|
|
WHERE ${conditions.join(' AND ')}
|
|
ORDER BY asset.name,asset.code
|
|
LIMIT ${limit}
|
|
`, params);
|
|
|
|
const hasMore = rows.length > 200;
|
|
const data = hasMore ? rows.slice(0,200) : rows;
|
|
return { parent, data, meta: { count: data.length, hasMore } };
|
|
}
|
|
|
|
private async parent(parentId: string): Promise<ParentContext> {
|
|
const [parent] = await this.dataSource.query(`
|
|
SELECT asset.id,asset.code,asset.name,type.code AS "typeCode"
|
|
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 ParentContext[];
|
|
if (!parent) {
|
|
throw new NotFoundException({ code:'INVENTORY_BROWSER_PARENT_NOT_FOUND',message:'El nivel de Inventario no existe' });
|
|
}
|
|
if (!['departamento','area','yacimiento','instalacion','subinstalacion'].includes(parent.typeCode.toLowerCase())) {
|
|
throw new BadRequestException({
|
|
code:'INVENTORY_BROWSER_PARENT_TYPE_INVALID',
|
|
message:'La navegación de Inventarios admite Departamento → Área → Yacimiento → Instalación → Subinstalación',
|
|
});
|
|
}
|
|
return parent;
|
|
}
|
|
|
|
private allowedChildType(typeCode: string): string | null {
|
|
switch (typeCode.toLowerCase()) {
|
|
case 'departamento': return 'area';
|
|
case 'area': return 'yacimiento';
|
|
case 'yacimiento': return 'instalacion';
|
|
case 'instalacion': return 'subinstalacion';
|
|
default: return null;
|
|
}
|
|
}
|
|
}
|