erp-construccion-backend/src/modules/budgets/services/concepto.service.ts

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[];
}