161 lines
3.5 KiB
TypeScript
161 lines
3.5 KiB
TypeScript
/**
|
|
* ConceptoService - Catalogo de Conceptos de Obra
|
|
*
|
|
* Gestiona el catálogo jerárquico de conceptos de obra.
|
|
* Los conceptos pueden tener estructura padre-hijo (niveles).
|
|
*
|
|
* @module Budgets
|
|
*/
|
|
|
|
import { Repository, IsNull } from 'typeorm';
|
|
import { BaseService, ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
|
import { Concepto } from '../entities/concepto.entity';
|
|
|
|
export interface CreateConceptoDto {
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
parentId?: string;
|
|
unitId?: string;
|
|
unitPrice?: number;
|
|
isComposite?: boolean;
|
|
}
|
|
|
|
export interface UpdateConceptoDto {
|
|
name?: string;
|
|
description?: string;
|
|
unitId?: string;
|
|
unitPrice?: number;
|
|
isComposite?: boolean;
|
|
}
|
|
|
|
export class ConceptoService extends BaseService<Concepto> {
|
|
constructor(repository: Repository<Concepto>) {
|
|
super(repository);
|
|
}
|
|
|
|
/**
|
|
* Crear un nuevo concepto con cálculo automático de nivel y path
|
|
*/
|
|
async createConcepto(
|
|
ctx: ServiceContext,
|
|
data: CreateConceptoDto
|
|
): Promise<Concepto> {
|
|
let level = 0;
|
|
let path = data.code;
|
|
|
|
if (data.parentId) {
|
|
const parent = await this.findById(ctx, data.parentId);
|
|
if (parent) {
|
|
level = parent.level + 1;
|
|
path = `${parent.path}/${data.code}`;
|
|
}
|
|
}
|
|
|
|
return this.create(ctx, {
|
|
...data,
|
|
level,
|
|
path,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Obtener conceptos raíz (sin padre)
|
|
*/
|
|
async findRootConceptos(
|
|
ctx: ServiceContext,
|
|
page = 1,
|
|
limit = 50
|
|
): Promise<PaginatedResult<Concepto>> {
|
|
return this.findAll(ctx, {
|
|
page,
|
|
limit,
|
|
where: { parentId: IsNull() } as any,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Obtener hijos de un concepto
|
|
*/
|
|
async findChildren(
|
|
ctx: ServiceContext,
|
|
parentId: string
|
|
): Promise<Concepto[]> {
|
|
return this.find(ctx, {
|
|
where: { parentId } as any,
|
|
order: { code: 'ASC' },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Obtener árbol completo de conceptos
|
|
*/
|
|
async getConceptoTree(
|
|
ctx: ServiceContext,
|
|
rootId?: string
|
|
): Promise<ConceptoNode[]> {
|
|
const where = rootId
|
|
? { parentId: rootId }
|
|
: { parentId: IsNull() };
|
|
|
|
const roots = await this.find(ctx, {
|
|
where: where as any,
|
|
order: { code: 'ASC' },
|
|
});
|
|
|
|
return this.buildTree(ctx, roots);
|
|
}
|
|
|
|
private async buildTree(
|
|
ctx: ServiceContext,
|
|
conceptos: Concepto[]
|
|
): Promise<ConceptoNode[]> {
|
|
const tree: ConceptoNode[] = [];
|
|
|
|
for (const concepto of conceptos) {
|
|
const children = await this.findChildren(ctx, concepto.id);
|
|
const childNodes = children.length > 0
|
|
? await this.buildTree(ctx, children)
|
|
: [];
|
|
|
|
tree.push({
|
|
...concepto,
|
|
children: childNodes,
|
|
});
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
/**
|
|
* Buscar conceptos por código o nombre
|
|
*/
|
|
async search(
|
|
ctx: ServiceContext,
|
|
term: string,
|
|
limit = 20
|
|
): Promise<Concepto[]> {
|
|
return this.repository
|
|
.createQueryBuilder('c')
|
|
.where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
|
.andWhere('c.deleted_at IS NULL')
|
|
.andWhere('(c.code ILIKE :term OR c.name ILIKE :term)', {
|
|
term: `%${term}%`,
|
|
})
|
|
.orderBy('c.code', 'ASC')
|
|
.take(limit)
|
|
.getMany();
|
|
}
|
|
|
|
/**
|
|
* Verificar si un código ya existe
|
|
*/
|
|
async codeExists(ctx: ServiceContext, code: string): Promise<boolean> {
|
|
return this.exists(ctx, { code } as any);
|
|
}
|
|
}
|
|
|
|
interface ConceptoNode extends Concepto {
|
|
children: ConceptoNode[];
|
|
}
|