import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IssueLinkType, IssuePriority, NotificationKind, ProjectRole, ReleaseStatus, SprintStatus, SETTING_COLORS, StatusCategory, TYPE_ICONS, } from '@task-stream/api/tracker/domain'; /* * REST response bodies. Same shapes as the GraphQL types (both are views of * the read models); here they only document the contract for OpenAPI. */ export class UserRefDto { @ApiProperty({ description: 'Pireon user id' }) id!: string; @ApiProperty() name!: string; } export class ProjectMemberDto { @ApiProperty({ type: UserRefDto }) user!: UserRefDto; @ApiProperty({ enum: ProjectRole, enumName: 'ProjectRole' }) role!: ProjectRole; } export class WorkflowStatusDto { @ApiProperty({ example: 'IN_REVIEW', description: 'Stable id on tickets' }) id!: string; @ApiProperty({ example: 'En revisión' }) name!: string; @ApiProperty({ enum: StatusCategory, enumName: 'StatusCategory' }) category!: StatusCategory; @ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' }) color!: string; @ApiProperty({ description: 'Entering it needs a reason' }) requiresReason!: boolean; @ApiProperty({ description: 'No new tickets or transitions into it' }) archived!: boolean; } export class StatusTransitionsDto { @ApiProperty({ example: 'IN_QA' }) from!: string; @ApiProperty({ type: [String], example: ['DONE', 'QA_FAILED'] }) to!: string[]; } export class IssueTypeOptionDto { @ApiProperty({ example: 'BUG' }) id!: string; @ApiProperty({ example: 'Bug' }) name!: string; @ApiProperty({ enum: TYPE_ICONS, enumName: 'TypeIcon' }) icon!: string; @ApiProperty({ enum: SETTING_COLORS, enumName: 'SettingColor' }) color!: string; @ApiProperty() archived!: boolean; } export class AreaOptionDto { @ApiProperty({ example: 'FRONTEND' }) id!: string; @ApiProperty({ example: 'Frontend' }) name!: string; @ApiProperty() archived!: boolean; } export class ProjectSettingsDto { @ApiProperty({ type: [WorkflowStatusDto], description: 'In board order' }) statuses!: WorkflowStatusDto[]; @ApiProperty({ type: [StatusTransitionsDto] }) transitions!: StatusTransitionsDto[]; @ApiProperty({ type: [IssueTypeOptionDto] }) issueTypes!: IssueTypeOptionDto[]; @ApiProperty({ type: [AreaOptionDto] }) areas!: AreaOptionDto[]; } export class ProjectDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ example: 'WEB', description: 'Prefix of the ticket keys' }) key!: string; @ApiProperty({ example: 'Checkout revamp' }) name!: string; @ApiProperty({ type: String, nullable: true }) description!: string | null; @ApiProperty({ description: 'Readable by anyone, also without a token' }) isPublic!: boolean; @ApiProperty({ type: UserRefDto }) owner!: UserRefDto; @ApiProperty({ type: [ProjectMemberDto] }) members!: ProjectMemberDto[]; @ApiProperty({ type: ProjectSettingsDto }) settings!: ProjectSettingsDto; @ApiProperty({ format: 'date-time' }) createdAt!: string; @ApiProperty({ format: 'date-time' }) updatedAt!: string; @ApiProperty({ description: 'Version of the aggregate (event count)' }) version!: number; } export class IssueLinkDto { @ApiProperty({ enum: IssueLinkType, enumName: 'IssueLinkType' }) type!: IssueLinkType; @ApiProperty({ enum: ['OUTWARD', 'INWARD'], enumName: 'LinkDirection', description: 'OUTWARD: this issue → other. INWARD: other → this issue.', }) direction!: 'OUTWARD' | 'INWARD'; @ApiProperty({ format: 'uuid' }) issueId!: string; @ApiProperty({ example: 'WEB-3' }) issueKey!: string; } export class ReleaseRefDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ example: 'v1.4.0' }) name!: string; } export class AttachmentDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ example: 'captura-login.png' }) fileName!: string; @ApiProperty({ example: 'image/png' }) mimeType!: string; @ApiProperty({ description: 'Bytes', example: 48213 }) size!: number; @ApiProperty({ type: UserRefDto }) uploadedBy!: UserRefDto; @ApiProperty({ format: 'date-time' }) uploadedAt!: string; @ApiProperty({ type: String, nullable: true, format: 'uuid', description: 'The comment it was attached to, if any', }) commentId!: string | null; } export class ChecklistItemDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ example: 'Actualizar el changelog' }) text!: string; @ApiProperty() done!: boolean; } export class IssueParentDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ example: 'WEB-3' }) key!: string; @ApiProperty({ example: 'Pago con tarjeta' }) title!: string; } export class SprintRefDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ example: 'Sprint 14' }) name!: string; } export class IssueDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ format: 'uuid' }) projectId!: string; @ApiProperty({ example: 'WEB' }) projectKey!: string; @ApiProperty({ example: 12 }) number!: number; @ApiProperty({ example: 'WEB-12', description: 'Human key' }) key!: string; @ApiProperty({ example: 'BUG', description: 'A ticket type id' }) type!: string; @ApiProperty({ type: String, nullable: true, example: 'FRONTEND' }) area!: string | null; @ApiProperty() title!: string; @ApiProperty({ type: String, nullable: true }) description!: string | null; @ApiProperty({ example: 'IN_PROGRESS', description: 'A status id' }) status!: string; @ApiProperty({ enum: StatusCategory, enumName: 'StatusCategory' }) statusCategory!: StatusCategory; @ApiProperty({ type: [String], description: "Statuses this ticket can move to next (project's workflow)", }) allowedTransitions!: string[]; @ApiProperty({ enum: IssuePriority, enumName: 'IssuePriority' }) priority!: IssuePriority; @ApiProperty({ type: UserRefDto }) reporter!: UserRefDto; @ApiProperty({ type: UserRefDto, nullable: true }) assignee!: UserRefDto | null; @ApiProperty({ type: ReleaseRefDto, nullable: true }) release!: ReleaseRefDto | null; @ApiProperty({ type: SprintRefDto, nullable: true }) sprint!: SprintRefDto | null; @ApiProperty({ type: [String] }) labels!: string[]; @ApiProperty({ type: String, nullable: true, description: 'Figma frame' }) figmaUrl!: string | null; @ApiProperty({ type: Number, nullable: true }) storyPoints!: number | null; @ApiProperty({ type: String, nullable: true, format: 'date' }) dueDate!: string | null; @ApiProperty({ type: [IssueLinkDto] }) links!: IssueLinkDto[]; @ApiProperty({ type: [AttachmentDto] }) attachments!: AttachmentDto[]; @ApiProperty({ type: [UserRefDto], description: 'People following it' }) watchers!: UserRefDto[]; @ApiProperty({ type: IssueParentDto, nullable: true, description: 'The epic or ticket this one breaks down', }) parent!: IssueParentDto | null; @ApiProperty({ description: 'Place in the backlog and its board column (ascending, as text)', }) rank!: string; @ApiProperty({ type: [ChecklistItemDto] }) checklist!: ChecklistItemDto[]; @ApiProperty() commentCount!: number; @ApiProperty({ format: 'date-time' }) createdAt!: string; @ApiProperty({ format: 'date-time' }) updatedAt!: string; @ApiProperty() version!: number; } export class SavedFilterCriteriaResponseDto { @ApiPropertyOptional({ type: [String] }) types?: string[]; @ApiPropertyOptional({ type: [String] }) areas?: string[]; @ApiPropertyOptional({ enum: IssuePriority, enumName: 'IssuePriority', isArray: true, }) priorities?: IssuePriority[]; @ApiPropertyOptional({ type: String, nullable: true }) assigneeId?: string | null; @ApiPropertyOptional() unassigned?: boolean; @ApiPropertyOptional({ type: String, nullable: true, format: 'uuid' }) releaseId?: string | null; @ApiPropertyOptional({ type: [String] }) labels?: string[]; @ApiPropertyOptional({ type: String, nullable: true }) search?: string | null; } export class SavedFilterDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ format: 'uuid' }) projectId!: string; @ApiProperty({ type: UserRefDto }) owner!: UserRefDto; @ApiProperty({ example: 'Bugs críticos de frontend' }) name!: string; @ApiProperty({ description: 'Visible to the whole project' }) shared!: boolean; @ApiProperty({ type: SavedFilterCriteriaResponseDto }) criteria!: SavedFilterCriteriaResponseDto; @ApiProperty({ format: 'date-time' }) createdAt!: string; } export class SavedFilterIdDto { @ApiProperty({ format: 'uuid' }) id!: string; } export class IssuePageDto { @ApiProperty({ type: [IssueDto] }) items!: IssueDto[]; @ApiProperty({ description: 'Total matches (for pagination)' }) total!: number; } export class BoardColumnDto { @ApiProperty({ example: 'IN_PROGRESS' }) status!: string; @ApiProperty({ type: [IssueDto] }) issues!: IssueDto[]; @ApiProperty({ description: 'Tickets in the column (may exceed `issues`)' }) total!: number; } export class CommentDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ format: 'uuid' }) issueId!: string; @ApiProperty({ type: UserRefDto }) author!: UserRefDto; @ApiProperty() body!: string; @ApiProperty({ format: 'date-time' }) createdAt!: string; @ApiProperty({ type: String, nullable: true, format: 'date-time' }) editedAt!: string | null; @ApiProperty() version!: number; } export class CommentPageDto { @ApiProperty({ type: [CommentDto] }) items!: CommentDto[]; @ApiProperty() total!: number; } export class SprintDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ format: 'uuid' }) projectId!: string; @ApiProperty({ example: 'Sprint 14' }) name!: string; @ApiProperty({ type: String, nullable: true }) goal!: string | null; @ApiProperty({ format: 'date' }) startDate!: string; @ApiProperty({ format: 'date', description: 'Last day, included' }) endDate!: string; @ApiProperty({ enum: SprintStatus, enumName: 'SprintStatus' }) status!: SprintStatus; @ApiProperty({ type: String, nullable: true, format: 'date-time' }) startedAt!: string | null; @ApiProperty({ type: String, nullable: true, format: 'date-time' }) closedAt!: string | null; @ApiProperty({ format: 'date-time' }) createdAt!: string; @ApiProperty() version!: number; } export class SprintProgressDto { @ApiProperty() total!: number; @ApiProperty({ description: 'Done or cancelled' }) finished!: number; @ApiProperty({ minimum: 0, maximum: 100 }) percent!: number; @ApiProperty() storyPoints!: number; @ApiProperty() finishedStoryPoints!: number; } export class SprintBurndownDto { @ApiProperty({ format: 'uuid' }) sprintId!: string; @ApiProperty({ type: [String], format: 'date' }) dates!: string[]; @ApiProperty({ type: [Number], nullable: true, description: 'Tickets in the sprint (null: days to come)', }) scope!: (number | null)[]; @ApiProperty({ type: [Number], nullable: true }) remaining!: ( | number | null )[]; @ApiProperty({ type: [Number], nullable: true }) remainingPoints!: (number | null)[]; @ApiProperty({ type: [Number] }) ideal!: number[]; } export class ReleaseDto { @ApiProperty({ format: 'uuid' }) id!: string; @ApiProperty({ format: 'uuid' }) projectId!: string; @ApiProperty({ example: 'v1.4.0' }) name!: string; @ApiProperty({ type: String, nullable: true }) description!: string | null; @ApiProperty({ type: String, nullable: true, format: 'date' }) targetDate!: string | null; @ApiProperty({ enum: ReleaseStatus, enumName: 'ReleaseStatus' }) status!: ReleaseStatus; @ApiProperty({ type: String, nullable: true, format: 'date-time' }) releasedAt!: string | null; @ApiProperty({ format: 'date-time' }) createdAt!: string; @ApiProperty() version!: number; } export class CountByStatusDto { @ApiProperty({ example: 'IN_PROGRESS', description: 'Status id' }) key!: string; @ApiProperty() count!: number; } export class CountByKeyDto { @ApiProperty({ description: 'Type, priority or area ("NONE" = unset)' }) key!: string; @ApiProperty() count!: number; } export class CountByAssigneeDto { @ApiProperty({ type: UserRefDto, nullable: true, description: 'Null = unassigned', }) user!: UserRefDto | null; @ApiProperty() count!: number; } export class ChildProgressDto { @ApiProperty() total!: number; @ApiProperty({ description: 'Done or cancelled' }) finished!: number; @ApiProperty({ minimum: 0, maximum: 100 }) percent!: number; @ApiProperty() storyPoints!: number; @ApiProperty() finishedStoryPoints!: number; } export class EpicDto { @ApiProperty({ type: IssueDto }) issue!: IssueDto; @ApiProperty({ type: ChildProgressDto }) progress!: ChildProgressDto; } export class ReleaseProgressDto { @ApiProperty() total!: number; @ApiProperty() finished!: number; @ApiProperty({ minimum: 0, maximum: 100 }) percent!: number; @ApiProperty({ type: [CountByStatusDto] }) byStatus!: CountByStatusDto[]; } export class ProjectStatsDto { @ApiProperty() total!: number; @ApiProperty({ description: 'Not done nor cancelled' }) open!: number; @ApiProperty({ type: [CountByStatusDto] }) byStatus!: CountByStatusDto[]; @ApiProperty({ type: [CountByKeyDto] }) byType!: CountByKeyDto[]; @ApiProperty({ type: [CountByKeyDto] }) byPriority!: CountByKeyDto[]; @ApiProperty({ type: [CountByKeyDto] }) byArea!: CountByKeyDto[]; @ApiProperty({ type: [CountByAssigneeDto], description: 'Open tickets per person', }) byAssignee!: CountByAssigneeDto[]; } export class ActivityDto { @ApiProperty() id!: string; @ApiProperty({ example: 'IssueStatusChanged' }) type!: string; @ApiProperty({ format: 'uuid' }) projectId!: string; @ApiProperty({ type: String, nullable: true }) issueId!: string | null; @ApiProperty({ type: String, nullable: true, example: 'WEB-12' }) issueKey!: string | null; @ApiProperty({ type: UserRefDto }) actor!: UserRefDto; @ApiProperty({ example: 'Ana movió WEB-12 de En curso a En revisión' }) summary!: string; @ApiProperty({ format: 'date-time' }) occurredAt!: string; } export class CommandResultDto { @ApiProperty({ description: 'Id of the aggregate written' }) id!: string; @ApiPropertyOptional({ description: 'Human key when one was assigned (project or issue)', example: 'WEB-13', }) key?: string; @ApiProperty({ description: 'Version after the write. Writes are acknowledged at once and the read ' + 'model catches up asynchronously: a GET may briefly return the previous ' + 'version (or 404 right after a create).', }) version!: number; } // ---- Statistics --------------------------------------------------------------- export class CreatedResolvedDto { @ApiProperty({ format: 'date', description: 'Monday of the week' }) week!: string; @ApiProperty() created!: number; @ApiProperty() resolved!: number; } export class FlowSeriesDto { @ApiProperty({ example: 'IN_PROGRESS' }) status!: string; @ApiProperty({ type: [Number], description: 'One value per date' }) counts!: number[]; } export class CumulativeFlowDto { @ApiProperty({ type: [String], format: 'date' }) dates!: string[]; @ApiProperty({ type: [FlowSeriesDto] }) series!: FlowSeriesDto[]; } export class DurationStatsDto { @ApiProperty() count!: number; @ApiProperty({ type: Number, nullable: true }) averageDays!: number | null; @ApiProperty({ type: Number, nullable: true, description: '85% of tickets take at most this long', }) p85Days!: number | null; } export class ThroughputDto { @ApiProperty({ format: 'date' }) week!: string; @ApiProperty({ type: UserRefDto, nullable: true }) user!: UserRefDto | null; @ApiProperty() count!: number; } export class QaStatsDto { @ApiProperty({ description: 'Left QA to a done status' }) passed!: number; @ApiProperty({ description: 'Left QA back to work' }) failed!: number; @ApiProperty({ type: Number, nullable: true, minimum: 0, maximum: 1 }) failureRate!: number | null; } export class ProjectAnalyticsDto { @ApiProperty({ format: 'date-time' }) from!: string; @ApiProperty({ format: 'date-time' }) to!: string; @ApiProperty({ type: [CreatedResolvedDto] }) createdVsResolved!: CreatedResolvedDto[]; @ApiProperty({ type: CumulativeFlowDto, description: 'Tickets in each status at the end of every day', }) cumulativeFlow!: CumulativeFlowDto; @ApiProperty({ type: DurationStatsDto, description: 'Start of work → done' }) cycleTime!: DurationStatsDto; @ApiProperty({ type: DurationStatsDto, description: 'Creation → done' }) leadTime!: DurationStatsDto; @ApiProperty({ type: [ThroughputDto] }) throughput!: ThroughputDto[]; @ApiProperty({ type: QaStatsDto }) qa!: QaStatsDto; } export class ReleaseBurnupDto { @ApiProperty({ format: 'uuid' }) releaseId!: string; @ApiProperty({ type: String, nullable: true, format: 'date' }) targetDate!: string | null; @ApiProperty({ type: [String], format: 'date' }) dates!: string[]; @ApiProperty({ type: [Number], description: 'Tickets in the release (not cancelled)', }) scope!: number[]; @ApiProperty({ type: [Number] }) done!: number[]; } // ---- Notifications ---------------------------------------------------------- export class NotificationDto { @ApiProperty() id!: string; @ApiProperty({ enum: NotificationKind, enumName: 'NotificationKind' }) kind!: NotificationKind; @ApiProperty({ format: 'uuid' }) projectId!: string; @ApiProperty({ example: 'WEB' }) projectKey!: string; @ApiProperty({ type: String, nullable: true, format: 'uuid' }) issueId!: string | null; @ApiProperty({ type: String, nullable: true, example: 'WEB-12' }) issueKey!: string | null; @ApiProperty({ type: UserRefDto }) actor!: UserRefDto; @ApiProperty({ example: 'Ana te asignó WEB-12: Login roto en Safari' }) summary!: string; @ApiProperty({ type: String, nullable: true, format: 'date-time', description: 'Null: unread', }) readAt!: string | null; @ApiProperty({ format: 'date-time' }) createdAt!: string; } export class MyNotificationsDto { @ApiProperty({ type: [NotificationDto] }) items!: NotificationDto[]; @ApiProperty({ description: 'All my unread ones, not just these' }) unread!: number; } export class NotificationSettingsResponseDto { @ApiProperty({ description: 'Copies by e-mail' }) email!: boolean; @ApiProperty({ description: 'Whether this installation can send e-mail' }) emailAvailable!: boolean; }