diff --git a/src/modules/contracts/services/contract.service.ts b/src/modules/contracts/services/contract.service.ts index 303b94d..0e799b1 100644 --- a/src/modules/contracts/services/contract.service.ts +++ b/src/modules/contracts/services/contract.service.ts @@ -9,6 +9,7 @@ import { Repository, FindOptionsWhere, LessThan } from 'typeorm'; import { Contract, ContractStatus, ContractType } from '../entities/contract.entity'; import { ContractAddendum } from '../entities/contract-addendum.entity'; +import { ContractPartida } from '../entities/contract-partida.entity'; interface ServiceContext { tenantId: string; @@ -48,6 +49,17 @@ export interface CreateAddendumDto { notes?: string; } +export interface CreatePartidaDto { + conceptoId: string; + quantity: number; + unitPrice: number; +} + +export interface UpdatePartidaDto { + quantity?: number; + unitPrice?: number; +} + export interface ContractFilters { projectId?: string; fraccionamientoId?: string; @@ -60,7 +72,8 @@ export interface ContractFilters { export class ContractService { constructor( private readonly contractRepository: Repository, - private readonly addendumRepository: Repository + private readonly addendumRepository: Repository, + private readonly partidaRepository?: Repository ) {} private generateContractNumber(type: ContractType): string { @@ -415,4 +428,133 @@ export class ContractService { order: { endDate: 'ASC' }, }); } + + // Contract Partidas (Items) Management + + async getPartidas(ctx: ServiceContext, contractId: string): Promise { + if (!this.partidaRepository) { + throw new Error('Partida repository not initialized'); + } + + return this.partidaRepository.find({ + where: { + contractId, + tenantId: ctx.tenantId, + deletedAt: null, + } as unknown as FindOptionsWhere, + order: { createdAt: 'ASC' }, + }); + } + + async addPartida(ctx: ServiceContext, contractId: string, dto: CreatePartidaDto): Promise { + if (!this.partidaRepository) { + throw new Error('Partida repository not initialized'); + } + + const contract = await this.findById(ctx, contractId); + if (!contract) { + throw new Error('Contract not found'); + } + + if (contract.status !== 'draft' && contract.status !== 'review') { + throw new Error('Can only add partidas to draft or review contracts'); + } + + const partida = this.partidaRepository.create({ + tenantId: ctx.tenantId, + contractId, + conceptoId: dto.conceptoId, + quantity: dto.quantity, + unitPrice: dto.unitPrice, + createdById: ctx.userId, + }); + + return this.partidaRepository.save(partida); + } + + async updatePartida( + ctx: ServiceContext, + partidaId: string, + dto: UpdatePartidaDto + ): Promise { + if (!this.partidaRepository) { + throw new Error('Partida repository not initialized'); + } + + const partida = await this.partidaRepository.findOne({ + where: { + id: partidaId, + tenantId: ctx.tenantId, + deletedAt: null, + } as unknown as FindOptionsWhere, + relations: ['contract'], + }); + + if (!partida) { + return null; + } + + if (partida.contract.status !== 'draft' && partida.contract.status !== 'review') { + throw new Error('Can only update partidas of draft or review contracts'); + } + + if (dto.quantity !== undefined) { + partida.quantity = dto.quantity; + } + if (dto.unitPrice !== undefined) { + partida.unitPrice = dto.unitPrice; + } + partida.updatedById = ctx.userId || ''; + + return this.partidaRepository.save(partida); + } + + async removePartida(ctx: ServiceContext, partidaId: string): Promise { + if (!this.partidaRepository) { + throw new Error('Partida repository not initialized'); + } + + const partida = await this.partidaRepository.findOne({ + where: { + id: partidaId, + tenantId: ctx.tenantId, + deletedAt: null, + } as unknown as FindOptionsWhere, + relations: ['contract'], + }); + + if (!partida) { + return false; + } + + if (partida.contract.status !== 'draft' && partida.contract.status !== 'review') { + throw new Error('Can only remove partidas from draft or review contracts'); + } + + await this.partidaRepository.update( + { id: partidaId, tenantId: ctx.tenantId } as FindOptionsWhere, + { deletedAt: new Date(), deletedById: ctx.userId || '' } + ); + + return true; + } + + async recalculateContractAmount(ctx: ServiceContext, contractId: string): Promise { + if (!this.partidaRepository) { + throw new Error('Partida repository not initialized'); + } + + const contract = await this.findById(ctx, contractId); + if (!contract) { + return null; + } + + const partidas = await this.getPartidas(ctx, contractId); + const totalAmount = partidas.reduce((sum, p) => sum + Number(p.quantity) * Number(p.unitPrice), 0); + + contract.contractAmount = totalAmount; + contract.updatedById = ctx.userId || ''; + + return this.contractRepository.save(contract); + } } diff --git a/src/modules/contracts/services/subcontractor.service.ts b/src/modules/contracts/services/subcontractor.service.ts index fc17cb4..74fe6dc 100644 --- a/src/modules/contracts/services/subcontractor.service.ts +++ b/src/modules/contracts/services/subcontractor.service.ts @@ -225,6 +225,22 @@ export class SubcontractorService { return this.subcontractorRepository.save(subcontractor); } + async reactivate(ctx: ServiceContext, id: string): Promise { + const subcontractor = await this.findById(ctx, id); + if (!subcontractor) { + return null; + } + + if (subcontractor.status === 'blacklisted') { + throw new Error('Cannot reactivate a blacklisted subcontractor'); + } + + subcontractor.status = 'active'; + subcontractor.updatedById = ctx.userId || ''; + + return this.subcontractorRepository.save(subcontractor); + } + async blacklist(ctx: ServiceContext, id: string, reason: string): Promise { const subcontractor = await this.findById(ctx, id); if (!subcontractor) { @@ -263,4 +279,42 @@ export class SubcontractorService { order: { averageRating: 'DESC' }, }); } + + async getTopRated(ctx: ServiceContext, limit: number = 10): Promise { + return this.subcontractorRepository.find({ + where: { + tenantId: ctx.tenantId, + status: 'active' as SubcontractorStatus, + deletedAt: null, + } as unknown as FindOptionsWhere, + order: { averageRating: 'DESC', completedContracts: 'DESC' }, + take: limit, + }); + } + + async getStatistics(ctx: ServiceContext): Promise<{ + total: number; + active: number; + inactive: number; + blacklisted: number; + averageRating: number; + }> { + const subcontractors = await this.subcontractorRepository.find({ + where: { + tenantId: ctx.tenantId, + deletedAt: null, + } as unknown as FindOptionsWhere, + }); + + const active = subcontractors.filter(s => s.status === 'active'); + const totalRating = active.reduce((sum, s) => sum + Number(s.averageRating), 0); + + return { + total: subcontractors.length, + active: active.length, + inactive: subcontractors.filter(s => s.status === 'inactive').length, + blacklisted: subcontractors.filter(s => s.status === 'blacklisted').length, + averageRating: active.length > 0 ? totalRating / active.length : 0, + }; + } }