import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, FindOptionsWhere, ILike, LessThanOrEqual, MoreThanOrEqual, Between } from 'typeorm'; import { DefinitionEntity, GoalStatus, } from '../entities/definition.entity'; import { CreateDefinitionDto, UpdateDefinitionDto, UpdateDefinitionStatusDto, DefinitionFiltersDto, DefinitionResponseDto, } from '../dto/definition.dto'; @Injectable() export class DefinitionsService { constructor( @InjectRepository(DefinitionEntity) private readonly definitionRepo: Repository, ) {} async create(tenantId: string, userId: string, dto: CreateDefinitionDto): Promise { // Validate date range if (new Date(dto.endsAt) < new Date(dto.startsAt)) { throw new BadRequestException('End date must be after start date'); } const definition = this.definitionRepo.create({ tenantId, createdBy: userId, ...dto, }); return this.definitionRepo.save(definition); } async findAll( tenantId: string, filters: DefinitionFiltersDto, ): Promise<{ items: DefinitionResponseDto[]; total: number; page: number; limit: number; totalPages: number }> { const { status, period, category, search, activeOn, sortBy = 'createdAt', sortOrder = 'DESC', page = 1, limit = 20, } = filters; const where: FindOptionsWhere = { tenantId }; if (status) where.status = status; if (period) where.period = period; if (category) where.category = category; if (search) where.name = ILike(`%${search}%`); const queryBuilder = this.definitionRepo .createQueryBuilder('d') .where('d.tenant_id = :tenantId', { tenantId }); if (status) queryBuilder.andWhere('d.status = :status', { status }); if (period) queryBuilder.andWhere('d.period = :period', { period }); if (category) queryBuilder.andWhere('d.category = :category', { category }); if (search) queryBuilder.andWhere('d.name ILIKE :search', { search: `%${search}%` }); if (activeOn) { queryBuilder.andWhere('d.starts_at <= :activeOn AND d.ends_at >= :activeOn', { activeOn }); } // Add assignment count queryBuilder .loadRelationCountAndMap('d.assignmentCount', 'd.assignments') .orderBy(`d.${this.camelToSnake(sortBy)}`, sortOrder) .skip((page - 1) * limit) .take(limit); const [items, total] = await queryBuilder.getManyAndCount(); return { items: items as DefinitionResponseDto[], total, page, limit, totalPages: Math.ceil(total / limit), }; } async findOne(tenantId: string, id: string): Promise { const definition = await this.definitionRepo.findOne({ where: { id, tenantId }, relations: ['creator'], }); if (!definition) { throw new NotFoundException(`Goal definition with ID ${id} not found`); } return definition; } async update(tenantId: string, id: string, dto: UpdateDefinitionDto): Promise { const definition = await this.findOne(tenantId, id); // Validate date range if both provided if (dto.startsAt && dto.endsAt) { if (new Date(dto.endsAt) < new Date(dto.startsAt)) { throw new BadRequestException('End date must be after start date'); } } else if (dto.startsAt && new Date(dto.startsAt) > definition.endsAt) { throw new BadRequestException('Start date must be before end date'); } else if (dto.endsAt && new Date(dto.endsAt) < definition.startsAt) { throw new BadRequestException('End date must be after start date'); } Object.assign(definition, dto); return this.definitionRepo.save(definition); } async updateStatus(tenantId: string, id: string, dto: UpdateDefinitionStatusDto): Promise { const definition = await this.findOne(tenantId, id); definition.status = dto.status; return this.definitionRepo.save(definition); } async activate(tenantId: string, id: string): Promise { return this.updateStatus(tenantId, id, { status: GoalStatus.ACTIVE }); } async duplicate(tenantId: string, id: string, userId: string): Promise { const original = await this.findOne(tenantId, id); const duplicate = this.definitionRepo.create({ ...original, id: undefined, name: `${original.name} (Copy)`, status: GoalStatus.DRAFT, createdBy: userId, createdAt: undefined, updatedAt: undefined, }); return this.definitionRepo.save(duplicate); } async remove(tenantId: string, id: string): Promise { const definition = await this.findOne(tenantId, id); await this.definitionRepo.remove(definition); } async getActiveGoals(tenantId: string): Promise { const today = new Date(); return this.definitionRepo.find({ where: { tenantId, status: GoalStatus.ACTIVE, startsAt: LessThanOrEqual(today), endsAt: MoreThanOrEqual(today), }, }); } private camelToSnake(str: string): string { return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } }