/** * 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 { constructor(repository: Repository) { super(repository); } /** * Crear un nuevo concepto con cálculo automático de nivel y path */ async createConcepto( ctx: ServiceContext, data: CreateConceptoDto ): Promise { 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> { return this.findAll(ctx, { page, limit, where: { parentId: IsNull() } as any, }); } /** * Obtener hijos de un concepto */ async findChildren( ctx: ServiceContext, parentId: string ): Promise { return this.find(ctx, { where: { parentId } as any, order: { code: 'ASC' }, }); } /** * Obtener árbol completo de conceptos */ async getConceptoTree( ctx: ServiceContext, rootId?: string ): Promise { 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 { 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 { 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 { return this.exists(ctx, { code } as any); } } interface ConceptoNode extends Concepto { children: ConceptoNode[]; }