template-saas-backend-v2/src/modules/goals/dto/assignment.dto.ts
Adrian Flores Cortes c4262498ee [CRIT-003] feat(validation): Add class-validator decorators to critical DTOs
- Add @Min(0) to monetary fields (prices, amounts)
- Add @Min(0)/@Max(100) to percentage fields (score, probability, rate)
- Add @IsEmail() to email fields with Spanish messages
- Add @IsUrl() to webhook URL field
- 10 DTOs updated with descriptive error messages in Spanish

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 17:23:09 -06:00

325 lines
9.2 KiB
TypeScript

import { IsString, IsOptional, IsEnum, IsNumber, IsUUID, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger';
import { AssigneeType, AssignmentStatus } from '../entities/assignment.entity';
import { ProgressSource } from '../entities/progress-log.entity';
// ─────────────────────────────────────────────
// Create Assignment DTO
// ─────────────────────────────────────────────
export class CreateAssignmentDto {
@ApiProperty()
@IsUUID('4', { message: 'El ID de definicion debe ser un UUID valido' })
definitionId: string;
@ApiPropertyOptional({ enum: AssigneeType, default: AssigneeType.USER })
@IsOptional()
@IsEnum(AssigneeType, { message: 'El tipo de asignado debe ser un valor valido' })
assigneeType?: AssigneeType;
@ApiPropertyOptional()
@IsOptional()
@IsUUID('4', { message: 'El ID de usuario debe ser un UUID valido' })
userId?: string;
@ApiPropertyOptional()
@IsOptional()
@IsUUID('4', { message: 'El ID de equipo debe ser un UUID valido' })
teamId?: string;
@ApiPropertyOptional({ example: 50000 })
@IsOptional()
@IsNumber({}, { message: 'El objetivo personalizado debe ser un numero' })
@Min(0, { message: 'El objetivo personalizado debe ser mayor o igual a 0' })
customTarget?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString({ message: 'Las notas deben ser una cadena de texto' })
notes?: string;
}
// ─────────────────────────────────────────────
// Update Assignment DTO
// ─────────────────────────────────────────────
export class UpdateAssignmentDto extends PartialType(CreateAssignmentDto) {}
// ─────────────────────────────────────────────
// Update Assignment Status DTO
// ─────────────────────────────────────────────
export class UpdateAssignmentStatusDto {
@ApiProperty({ enum: AssignmentStatus })
@IsEnum(AssignmentStatus)
status: AssignmentStatus;
}
// ─────────────────────────────────────────────
// Update Progress DTO
// ─────────────────────────────────────────────
export class UpdateProgressDto {
@ApiProperty({ example: 25000 })
@IsNumber({}, { message: 'El valor debe ser un numero' })
@Min(0, { message: 'El valor debe ser mayor o igual a 0' })
value: number;
@ApiPropertyOptional({ enum: ProgressSource, default: ProgressSource.MANUAL })
@IsOptional()
@IsEnum(ProgressSource, { message: 'La fuente de progreso debe ser un valor valido' })
source?: ProgressSource;
@ApiPropertyOptional()
@IsOptional()
@IsString({ message: 'La referencia de fuente debe ser una cadena de texto' })
sourceReference?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString({ message: 'Las notas deben ser una cadena de texto' })
notes?: string;
}
// ─────────────────────────────────────────────
// Assignment Response DTO
// ─────────────────────────────────────────────
export class AssignmentResponseDto {
@ApiProperty()
id: string;
@ApiProperty()
tenantId: string;
@ApiProperty()
definitionId: string;
@ApiProperty({ enum: AssigneeType })
assigneeType: AssigneeType;
@ApiPropertyOptional()
userId: string | null;
@ApiPropertyOptional()
teamId: string | null;
@ApiPropertyOptional()
customTarget: number | null;
@ApiProperty()
currentValue: number;
@ApiProperty()
progressPercentage: number;
@ApiPropertyOptional()
lastUpdatedAt: Date | null;
@ApiProperty({ enum: AssignmentStatus })
status: AssignmentStatus;
@ApiPropertyOptional()
achievedAt: Date | null;
@ApiPropertyOptional()
notes: string | null;
@ApiProperty()
createdAt: Date;
@ApiProperty()
updatedAt: Date;
// Nested objects
@ApiPropertyOptional()
definition?: {
id: string;
name: string;
targetValue: number;
unit: string | null;
startsAt: Date;
endsAt: Date;
};
@ApiPropertyOptional()
user?: {
id: string;
email: string;
firstName: string | null;
lastName: string | null;
};
}
// ─────────────────────────────────────────────
// Assignment Filters DTO
// ─────────────────────────────────────────────
export class AssignmentFiltersDto {
@ApiPropertyOptional()
@IsOptional()
@IsUUID('4', { message: 'El ID de definicion debe ser un UUID valido' })
definitionId?: string;
@ApiPropertyOptional()
@IsOptional()
@IsUUID('4', { message: 'El ID de usuario debe ser un UUID valido' })
userId?: string;
@ApiPropertyOptional({ enum: AssignmentStatus })
@IsOptional()
@IsEnum(AssignmentStatus, { message: 'El estado debe ser un valor valido' })
status?: AssignmentStatus;
@ApiPropertyOptional({ enum: AssigneeType })
@IsOptional()
@IsEnum(AssigneeType, { message: 'El tipo de asignado debe ser un valor valido' })
assigneeType?: AssigneeType;
@ApiPropertyOptional({ description: 'Minimum progress percentage' })
@IsOptional()
@Type(() => Number)
@IsNumber({}, { message: 'El progreso minimo debe ser un numero' })
@Min(0, { message: 'El progreso minimo debe ser mayor o igual a 0%' })
@Max(100, { message: 'El progreso minimo debe ser menor o igual a 100%' })
minProgress?: number;
@ApiPropertyOptional({ description: 'Maximum progress percentage' })
@IsOptional()
@Type(() => Number)
@IsNumber({}, { message: 'El progreso maximo debe ser un numero' })
@Min(0, { message: 'El progreso maximo debe ser mayor o igual a 0%' })
@Max(100, { message: 'El progreso maximo debe ser menor o igual a 100%' })
maxProgress?: number;
@ApiPropertyOptional({ example: 'progressPercentage' })
@IsOptional()
@IsString()
sortBy?: string;
@ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'DESC' })
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC';
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
page?: number;
@ApiPropertyOptional({ default: 20 })
@IsOptional()
@Type(() => Number)
@IsNumber()
limit?: number;
}
// ─────────────────────────────────────────────
// Progress Log Response DTO
// ─────────────────────────────────────────────
export class ProgressLogResponseDto {
@ApiProperty()
id: string;
@ApiProperty()
assignmentId: string;
@ApiPropertyOptional()
previousValue: number | null;
@ApiProperty()
newValue: number;
@ApiPropertyOptional()
changeAmount: number | null;
@ApiProperty({ enum: ProgressSource })
source: ProgressSource;
@ApiPropertyOptional()
sourceReference: string | null;
@ApiPropertyOptional()
notes: string | null;
@ApiProperty()
loggedAt: Date;
@ApiPropertyOptional()
loggedBy: string | null;
}
// ─────────────────────────────────────────────
// My Goals Summary DTO
// ─────────────────────────────────────────────
export class MyGoalsSummaryDto {
@ApiProperty()
totalAssignments: number;
@ApiProperty()
activeAssignments: number;
@ApiProperty()
achievedAssignments: number;
@ApiProperty()
failedAssignments: number;
@ApiProperty()
averageProgress: number;
@ApiProperty()
atRiskCount: number; // < 50% progress with > 75% time elapsed
}
// ─────────────────────────────────────────────
// Goal Report DTOs
// ─────────────────────────────────────────────
export class CompletionReportDto {
@ApiProperty()
totalGoals: number;
@ApiProperty()
achievedGoals: number;
@ApiProperty()
failedGoals: number;
@ApiProperty()
activeGoals: number;
@ApiProperty()
completionRate: number;
@ApiProperty()
averageProgress: number;
}
export class UserReportDto {
@ApiProperty()
userId: string;
@ApiPropertyOptional()
userName: string | null;
@ApiProperty()
totalAssignments: number;
@ApiProperty()
achieved: number;
@ApiProperty()
failed: number;
@ApiProperty()
active: number;
@ApiProperty()
averageProgress: number;
}