- Added 4 entities: DefinitionEntity, AssignmentEntity, ProgressLogEntity, MilestoneNotificationEntity - Added DTOs for definitions and assignments - Added services for definitions and assignments CRUD - Added controllers with full REST API endpoints - Added GoalsModule and registered in AppModule Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
169 lines
5.2 KiB
TypeScript
169 lines
5.2 KiB
TypeScript
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<DefinitionEntity>,
|
|
) {}
|
|
|
|
async create(tenantId: string, userId: string, dto: CreateDefinitionDto): Promise<DefinitionEntity> {
|
|
// 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<DefinitionEntity> = { 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<DefinitionEntity> {
|
|
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<DefinitionEntity> {
|
|
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<DefinitionEntity> {
|
|
const definition = await this.findOne(tenantId, id);
|
|
definition.status = dto.status;
|
|
return this.definitionRepo.save(definition);
|
|
}
|
|
|
|
async activate(tenantId: string, id: string): Promise<DefinitionEntity> {
|
|
return this.updateStatus(tenantId, id, { status: GoalStatus.ACTIVE });
|
|
}
|
|
|
|
async duplicate(tenantId: string, id: string, userId: string): Promise<DefinitionEntity> {
|
|
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<void> {
|
|
const definition = await this.findOne(tenantId, id);
|
|
await this.definitionRepo.remove(definition);
|
|
}
|
|
|
|
async getActiveGoals(tenantId: string): Promise<DefinitionEntity[]> {
|
|
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()}`);
|
|
}
|
|
}
|